Merge branch 'main' into notifications
This commit is contained in:
commit
b07f9fe3b5
61 changed files with 3185 additions and 1148 deletions
4
.github/workflows/release_actions.yml
vendored
4
.github/workflows/release_actions.yml
vendored
|
@ -20,9 +20,7 @@ jobs:
|
||||||
id: get-content
|
id: get-content
|
||||||
with:
|
with:
|
||||||
stringToTruncate: |
|
stringToTruncate: |
|
||||||
📣 Zed ${{ github.event.release.tag_name }} was just released!
|
📣 Zed [${{ github.event.release.tag_name }}](${{ steps.get-release-url.outputs.URL }}) was just released!
|
||||||
|
|
||||||
Restart your Zed or head to ${{ steps.get-release-url.outputs.URL }} to grab it.
|
|
||||||
|
|
||||||
${{ github.event.release.body }}
|
${{ github.event.release.body }}
|
||||||
maxLength: 2000
|
maxLength: 2000
|
||||||
|
|
28
Cargo.lock
generated
28
Cargo.lock
generated
|
@ -103,7 +103,7 @@ dependencies = [
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tiktoken-rs 0.5.4",
|
"tiktoken-rs",
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -316,12 +316,13 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"schemars",
|
"schemars",
|
||||||
"search",
|
"search",
|
||||||
|
"semantic_index",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
"theme",
|
"theme",
|
||||||
"tiktoken-rs 0.4.5",
|
"tiktoken-rs",
|
||||||
"util",
|
"util",
|
||||||
"uuid 1.4.1",
|
"uuid 1.4.1",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
@ -1466,7 +1467,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.24.0"
|
version = "0.25.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
@ -1629,6 +1630,7 @@ dependencies = [
|
||||||
"theme",
|
"theme",
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
"zed-actions",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -6975,7 +6977,7 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
"theme",
|
"theme",
|
||||||
"tiktoken-rs 0.5.4",
|
"tiktoken-rs",
|
||||||
"tree-sitter",
|
"tree-sitter",
|
||||||
"tree-sitter-cpp",
|
"tree-sitter-cpp",
|
||||||
"tree-sitter-elixir",
|
"tree-sitter-elixir",
|
||||||
|
@ -8166,21 +8168,6 @@ dependencies = [
|
||||||
"weezl",
|
"weezl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tiktoken-rs"
|
|
||||||
version = "0.4.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "52aacc1cff93ba9d5f198c62c49c77fa0355025c729eed3326beaf7f33bc8614"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"base64 0.21.4",
|
|
||||||
"bstr",
|
|
||||||
"fancy-regex",
|
|
||||||
"lazy_static",
|
|
||||||
"parking_lot 0.12.1",
|
|
||||||
"rustc-hash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiktoken-rs"
|
name = "tiktoken-rs"
|
||||||
version = "0.5.4"
|
version = "0.5.4"
|
||||||
|
@ -10103,7 +10090,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.109.0"
|
version = "0.110.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"ai",
|
"ai",
|
||||||
|
@ -10238,6 +10225,7 @@ name = "zed-actions"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
2
Procfile
2
Procfile
|
@ -1,4 +1,4 @@
|
||||||
web: cd ../zed.dev && PORT=3000 npm run dev
|
web: cd ../zed.dev && PORT=3000 npm run dev
|
||||||
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-collab=info} cargo run serve
|
collab: cd crates/collab && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve
|
||||||
livekit: livekit-server --dev
|
livekit: livekit-server --dev
|
||||||
postgrest: postgrest crates/collab/admin_api.conf
|
postgrest: postgrest crates/collab/admin_api.conf
|
||||||
|
|
3
assets/icons/link.svg
Normal file
3
assets/icons/link.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.51192 3.00541C9.18827 2.54594 10.0434 2.53694 10.6788 2.95419C10.823 3.04893 10.9771 3.1993 11.389 3.61119C11.8009 4.02307 11.9513 4.17714 12.046 4.32141C12.4632 4.95675 12.4542 5.81192 11.9948 6.48827C11.8899 6.64264 11.7276 6.80811 11.3006 7.23511L10.6819 7.85383C10.4866 8.04909 10.4866 8.36567 10.6819 8.56093C10.8772 8.7562 11.1937 8.7562 11.389 8.56093L12.0077 7.94221L12.0507 7.89929C12.4203 7.52976 12.6568 7.2933 12.822 7.0502C13.4972 6.05623 13.5321 4.76252 12.8819 3.77248C12.7233 3.53102 12.4922 3.30001 12.1408 2.94871L12.0961 2.90408L12.0515 2.85942C11.7002 2.508 11.4692 2.27689 11.2277 2.11832C10.2377 1.46813 8.94396 1.50299 7.94999 2.17822C7.70689 2.34336 7.47042 2.57991 7.10088 2.94955L7.05797 2.99247L6.43926 3.61119C6.24399 3.80645 6.24399 4.12303 6.43926 4.31829C6.63452 4.51355 6.9511 4.51355 7.14636 4.31829L7.76508 3.69957C8.19208 3.27257 8.35755 3.11027 8.51192 3.00541ZM4.31794 7.14672C4.5132 6.95146 4.5132 6.63487 4.31794 6.43961C4.12267 6.24435 3.80609 6.24435 3.61083 6.43961L2.99211 7.05833L2.9492 7.10124C2.57955 7.47077 2.34301 7.70724 2.17786 7.95035C1.50263 8.94432 1.46778 10.238 2.11797 11.2281C2.27654 11.4695 2.50764 11.7005 2.85908 12.0518L2.90372 12.0965L2.94835 12.1411C3.29965 12.4925 3.53066 12.7237 3.77212 12.8822C4.76217 13.5324 6.05587 13.4976 7.04984 12.8223C7.29294 12.6572 7.52941 12.4206 7.89894 12.051L7.89895 12.051L7.94186 12.0081L8.56058 11.3894C8.75584 11.1941 8.75584 10.8775 8.56058 10.6823C8.36531 10.487 8.04873 10.487 7.85347 10.6823L7.23475 11.301C6.80775 11.728 6.64228 11.8903 6.48792 11.9951C5.81156 12.4546 4.9564 12.4636 4.32105 12.0464C4.17679 11.9516 4.02272 11.8012 3.61083 11.3894C3.19894 10.9775 3.04858 10.8234 2.95383 10.6791C2.53659 10.0438 2.54558 9.18863 3.00505 8.51227C3.10991 8.35791 3.27222 8.19244 3.69922 7.76544L4.31794 7.14672ZM9.6217 6.08558C9.81696 5.89032 9.81696 5.57373 9.6217 5.37847C9.42644 5.18321 9.10986 5.18321 8.91459 5.37847L5.37906 8.91401C5.1838 9.10927 5.1838 9.42585 5.37906 9.62111C5.57432 9.81637 5.8909 9.81637 6.08617 9.62111L9.6217 6.08558Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
3
assets/icons/public.svg
Normal file
3
assets/icons/public.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.74393 2.00204C3.41963 1.97524 3.13502 2.37572 3.10823 2.70001C3.08143 3.0243 3.32558 3.47321 3.64986 3.50001C7.99878 3.85934 11.1406 7.00122 11.5 11.3501C11.5267 11.6744 11.9756 12.0269 12.3 12C12.6243 11.9733 13.0247 11.5804 12.998 11.2561C12.5912 6.33295 8.66704 2.40882 3.74393 2.00204ZM2.9 6.00001C2.96411 5.68099 3.33084 5.29361 3.64986 5.35772C6.66377 5.96341 9.03654 8.33618 9.64223 11.3501C9.70634 11.6691 9.319 12.0359 8.99999 12.1C8.68097 12.1641 8.06411 11.819 7.99999 11.5C7.48788 8.95167 6.0483 7.51213 3.49999 7.00001C3.18097 6.9359 2.8359 6.31902 2.9 6.00001ZM2 9.20001C2.0641 8.88099 2.38635 8.65788 2.70537 8.722C4.50255 9.08317 5.91684 10.4975 6.27801 12.2946C6.34212 12.6137 6.13547 12.9242 5.81646 12.9883C5.49744 13.0525 4.86411 12.819 4.8 12.5C4.53239 11.1683 3.83158 10.4676 2.5 10.2C2.18098 10.1359 1.93588 9.51902 2 9.20001Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1,021 B |
8
assets/icons/update.svg
Normal file
8
assets/icons/update.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M1.90321 7.29677C1.90321 10.341 4.11041 12.4147 6.58893 12.8439C6.87255 12.893 7.06266 13.1627 7.01355 13.4464C6.96444 13.73 6.69471 13.9201 6.41109 13.871C3.49942 13.3668 0.86084 10.9127 0.86084 7.29677C0.860839 5.76009 1.55996 4.55245 2.37639 3.63377C2.96124 2.97568 3.63034 2.44135 4.16846 2.03202L2.53205 2.03202C2.25591 2.03202 2.03205 1.80816 2.03205 1.53202C2.03205 1.25588 2.25591 1.03202 2.53205 1.03202L5.53205 1.03202C5.80819 1.03202 6.03205 1.25588 6.03205 1.53202L6.03205 4.53202C6.03205 4.80816 5.80819 5.03202 5.53205 5.03202C5.25591 5.03202 5.03205 4.80816 5.03205 4.53202L5.03205 2.68645L5.03054 2.68759L5.03045 2.68766L5.03044 2.68767L5.03043 2.68767C4.45896 3.11868 3.76059 3.64538 3.15554 4.3262C2.44102 5.13021 1.90321 6.10154 1.90321 7.29677ZM13.0109 7.70321C13.0109 4.69115 10.8505 2.6296 8.40384 2.17029C8.12093 2.11718 7.93465 1.84479 7.98776 1.56188C8.04087 1.27898 8.31326 1.0927 8.59616 1.14581C11.4704 1.68541 14.0532 4.12605 14.0532 7.70321C14.0532 9.23988 13.3541 10.4475 12.5377 11.3662C11.9528 12.0243 11.2837 12.5586 10.7456 12.968L12.3821 12.968C12.6582 12.968 12.8821 13.1918 12.8821 13.468C12.8821 13.7441 12.6582 13.968 12.3821 13.968L9.38205 13.968C9.10591 13.968 8.88205 13.7441 8.88205 13.468L8.88205 10.468C8.88205 10.1918 9.10591 9.96796 9.38205 9.96796C9.65819 9.96796 9.88205 10.1918 9.88205 10.468L9.88205 12.3135L9.88362 12.3123C10.4551 11.8813 11.1535 11.3546 11.7585 10.6738C12.4731 9.86976 13.0109 8.89844 13.0109 7.70321Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -85,25 +85,6 @@ impl Embedding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// impl FromSql for Embedding {
|
|
||||||
// fn column_result(value: ValueRef) -> FromSqlResult<Self> {
|
|
||||||
// let bytes = value.as_blob()?;
|
|
||||||
// let embedding: Result<Vec<f32>, Box<bincode::ErrorKind>> = bincode::deserialize(bytes);
|
|
||||||
// if embedding.is_err() {
|
|
||||||
// return Err(rusqlite::types::FromSqlError::Other(embedding.unwrap_err()));
|
|
||||||
// }
|
|
||||||
// Ok(Embedding(embedding.unwrap()))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// impl ToSql for Embedding {
|
|
||||||
// fn to_sql(&self) -> rusqlite::Result<ToSqlOutput> {
|
|
||||||
// let bytes = bincode::serialize(&self.0)
|
|
||||||
// .map_err(|err| rusqlite::Error::ToSqlConversionFailure(Box::new(err)))?;
|
|
||||||
// Ok(ToSqlOutput::Owned(rusqlite::types::Value::Blob(bytes)))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OpenAIEmbeddings {
|
pub struct OpenAIEmbeddings {
|
||||||
pub client: Arc<dyn HttpClient>,
|
pub client: Arc<dyn HttpClient>,
|
||||||
|
@ -300,6 +281,7 @@ impl EmbeddingProvider for OpenAIEmbeddings {
|
||||||
request_timeout,
|
request_timeout,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
request_number += 1;
|
request_number += 1;
|
||||||
|
|
||||||
match response.status() {
|
match response.status() {
|
||||||
|
|
|
@ -22,8 +22,11 @@ settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
uuid.workspace = true
|
semantic_index = { path = "../semantic_index" }
|
||||||
|
project = { path = "../project" }
|
||||||
|
|
||||||
|
uuid.workspace = true
|
||||||
|
log.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
@ -36,7 +39,7 @@ schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
tiktoken-rs = "0.4"
|
tiktoken-rs = "0.5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
editor = { path = "../editor", features = ["test-support"] }
|
editor = { path = "../editor", features = ["test-support"] }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
|
assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
|
||||||
codegen::{self, Codegen, CodegenKind},
|
codegen::{self, Codegen, CodegenKind},
|
||||||
prompts::generate_content_prompt,
|
prompts::{generate_content_prompt, PromptCodeSnippet},
|
||||||
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
|
MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
|
||||||
SavedMessage,
|
SavedMessage,
|
||||||
};
|
};
|
||||||
|
@ -29,13 +29,15 @@ use gpui::{
|
||||||
},
|
},
|
||||||
fonts::HighlightStyle,
|
fonts::HighlightStyle,
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||||
Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
|
Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelContext,
|
||||||
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
|
||||||
WindowContext,
|
WeakModelHandle, WeakViewHandle, WindowContext,
|
||||||
};
|
};
|
||||||
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
|
use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
|
||||||
|
use project::Project;
|
||||||
use search::BufferSearchBar;
|
use search::BufferSearchBar;
|
||||||
|
use semantic_index::{SemanticIndex, SemanticIndexStatus};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, RefCell},
|
cell::{Cell, RefCell},
|
||||||
|
@ -46,7 +48,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
use theme::{
|
use theme::{
|
||||||
components::{action_button::Button, ComponentExt},
|
components::{action_button::Button, ComponentExt},
|
||||||
|
@ -72,6 +74,7 @@ actions!(
|
||||||
ResetKey,
|
ResetKey,
|
||||||
InlineAssist,
|
InlineAssist,
|
||||||
ToggleIncludeConversation,
|
ToggleIncludeConversation,
|
||||||
|
ToggleRetrieveContext,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -108,6 +111,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(InlineAssistant::confirm);
|
cx.add_action(InlineAssistant::confirm);
|
||||||
cx.add_action(InlineAssistant::cancel);
|
cx.add_action(InlineAssistant::cancel);
|
||||||
cx.add_action(InlineAssistant::toggle_include_conversation);
|
cx.add_action(InlineAssistant::toggle_include_conversation);
|
||||||
|
cx.add_action(InlineAssistant::toggle_retrieve_context);
|
||||||
cx.add_action(InlineAssistant::move_up);
|
cx.add_action(InlineAssistant::move_up);
|
||||||
cx.add_action(InlineAssistant::move_down);
|
cx.add_action(InlineAssistant::move_down);
|
||||||
}
|
}
|
||||||
|
@ -145,6 +149,8 @@ pub struct AssistantPanel {
|
||||||
include_conversation_in_next_inline_assist: bool,
|
include_conversation_in_next_inline_assist: bool,
|
||||||
inline_prompt_history: VecDeque<String>,
|
inline_prompt_history: VecDeque<String>,
|
||||||
_watch_saved_conversations: Task<Result<()>>,
|
_watch_saved_conversations: Task<Result<()>>,
|
||||||
|
semantic_index: Option<ModelHandle<SemanticIndex>>,
|
||||||
|
retrieve_context_in_next_inline_assist: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssistantPanel {
|
impl AssistantPanel {
|
||||||
|
@ -191,6 +197,9 @@ impl AssistantPanel {
|
||||||
toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
|
toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
|
||||||
toolbar
|
toolbar
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let semantic_index = SemanticIndex::global(cx);
|
||||||
|
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
workspace: workspace_handle,
|
workspace: workspace_handle,
|
||||||
active_editor_index: Default::default(),
|
active_editor_index: Default::default(),
|
||||||
|
@ -215,6 +224,8 @@ impl AssistantPanel {
|
||||||
include_conversation_in_next_inline_assist: false,
|
include_conversation_in_next_inline_assist: false,
|
||||||
inline_prompt_history: Default::default(),
|
inline_prompt_history: Default::default(),
|
||||||
_watch_saved_conversations,
|
_watch_saved_conversations,
|
||||||
|
semantic_index,
|
||||||
|
retrieve_context_in_next_inline_assist: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut old_dock_position = this.position(cx);
|
let mut old_dock_position = this.position(cx);
|
||||||
|
@ -262,12 +273,19 @@ impl AssistantPanel {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let project = workspace.project();
|
||||||
|
|
||||||
this.update(cx, |assistant, cx| {
|
this.update(cx, |assistant, cx| {
|
||||||
assistant.new_inline_assist(&active_editor, cx)
|
assistant.new_inline_assist(&active_editor, cx, project)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_inline_assist(&mut self, editor: &ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
fn new_inline_assist(
|
||||||
|
&mut self,
|
||||||
|
editor: &ViewHandle<Editor>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
|
) {
|
||||||
let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
|
let api_key = if let Some(api_key) = self.api_key.borrow().clone() {
|
||||||
api_key
|
api_key
|
||||||
} else {
|
} else {
|
||||||
|
@ -312,6 +330,27 @@ impl AssistantPanel {
|
||||||
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(semantic_index) = self.semantic_index.clone() {
|
||||||
|
let project = project.clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let previously_indexed = semantic_index
|
||||||
|
.update(&mut cx, |index, cx| {
|
||||||
|
index.project_previously_indexed(&project, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or(false);
|
||||||
|
if previously_indexed {
|
||||||
|
let _ = semantic_index
|
||||||
|
.update(&mut cx, |index, cx| {
|
||||||
|
index.index_project(project.clone(), cx)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
|
let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
|
||||||
let inline_assistant = cx.add_view(|cx| {
|
let inline_assistant = cx.add_view(|cx| {
|
||||||
let assistant = InlineAssistant::new(
|
let assistant = InlineAssistant::new(
|
||||||
|
@ -322,6 +361,9 @@ impl AssistantPanel {
|
||||||
codegen.clone(),
|
codegen.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
self.retrieve_context_in_next_inline_assist,
|
||||||
|
self.semantic_index.clone(),
|
||||||
|
project.clone(),
|
||||||
);
|
);
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
assistant
|
assistant
|
||||||
|
@ -362,6 +404,7 @@ impl AssistantPanel {
|
||||||
editor: editor.downgrade(),
|
editor: editor.downgrade(),
|
||||||
inline_assistant: Some((block_id, inline_assistant.clone())),
|
inline_assistant: Some((block_id, inline_assistant.clone())),
|
||||||
codegen: codegen.clone(),
|
codegen: codegen.clone(),
|
||||||
|
project: project.downgrade(),
|
||||||
_subscriptions: vec![
|
_subscriptions: vec![
|
||||||
cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
|
cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
|
||||||
cx.subscribe(editor, {
|
cx.subscribe(editor, {
|
||||||
|
@ -440,8 +483,15 @@ impl AssistantPanel {
|
||||||
InlineAssistantEvent::Confirmed {
|
InlineAssistantEvent::Confirmed {
|
||||||
prompt,
|
prompt,
|
||||||
include_conversation,
|
include_conversation,
|
||||||
|
retrieve_context,
|
||||||
} => {
|
} => {
|
||||||
self.confirm_inline_assist(assist_id, prompt, *include_conversation, cx);
|
self.confirm_inline_assist(
|
||||||
|
assist_id,
|
||||||
|
prompt,
|
||||||
|
*include_conversation,
|
||||||
|
cx,
|
||||||
|
*retrieve_context,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
InlineAssistantEvent::Canceled => {
|
InlineAssistantEvent::Canceled => {
|
||||||
self.finish_inline_assist(assist_id, true, cx);
|
self.finish_inline_assist(assist_id, true, cx);
|
||||||
|
@ -454,6 +504,9 @@ impl AssistantPanel {
|
||||||
} => {
|
} => {
|
||||||
self.include_conversation_in_next_inline_assist = *include_conversation;
|
self.include_conversation_in_next_inline_assist = *include_conversation;
|
||||||
}
|
}
|
||||||
|
InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => {
|
||||||
|
self.retrieve_context_in_next_inline_assist = *retrieve_context
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -532,6 +585,7 @@ impl AssistantPanel {
|
||||||
user_prompt: &str,
|
user_prompt: &str,
|
||||||
include_conversation: bool,
|
include_conversation: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
|
retrieve_context: bool,
|
||||||
) {
|
) {
|
||||||
let conversation = if include_conversation {
|
let conversation = if include_conversation {
|
||||||
self.active_editor()
|
self.active_editor()
|
||||||
|
@ -553,6 +607,8 @@ impl AssistantPanel {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let project = pending_assist.project.clone();
|
||||||
|
|
||||||
self.inline_prompt_history
|
self.inline_prompt_history
|
||||||
.retain(|prompt| prompt != user_prompt);
|
.retain(|prompt| prompt != user_prompt);
|
||||||
self.inline_prompt_history.push_back(user_prompt.into());
|
self.inline_prompt_history.push_back(user_prompt.into());
|
||||||
|
@ -593,10 +649,62 @@ impl AssistantPanel {
|
||||||
let codegen_kind = codegen.read(cx).kind().clone();
|
let codegen_kind = codegen.read(cx).kind().clone();
|
||||||
let user_prompt = user_prompt.to_string();
|
let user_prompt = user_prompt.to_string();
|
||||||
|
|
||||||
let mut messages = Vec::new();
|
let snippets = if retrieve_context {
|
||||||
|
let Some(project) = project.upgrade(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_results = if let Some(semantic_index) = self.semantic_index.clone() {
|
||||||
|
let search_results = semantic_index.update(cx, |this, cx| {
|
||||||
|
this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move { search_results.await.unwrap_or_default() })
|
||||||
|
} else {
|
||||||
|
Task::ready(Vec::new())
|
||||||
|
};
|
||||||
|
|
||||||
|
let snippets = cx.spawn(|_, cx| async move {
|
||||||
|
let mut snippets = Vec::new();
|
||||||
|
for result in search_results.await {
|
||||||
|
snippets.push(PromptCodeSnippet::new(result, &cx));
|
||||||
|
|
||||||
|
// snippets.push(result.buffer.read_with(&cx, |buffer, _| {
|
||||||
|
// buffer
|
||||||
|
// .snapshot()
|
||||||
|
// .text_for_range(result.range)
|
||||||
|
// .collect::<String>()
|
||||||
|
// }));
|
||||||
|
}
|
||||||
|
snippets
|
||||||
|
});
|
||||||
|
snippets
|
||||||
|
} else {
|
||||||
|
Task::ready(Vec::new())
|
||||||
|
};
|
||||||
|
|
||||||
let mut model = settings::get::<AssistantSettings>(cx)
|
let mut model = settings::get::<AssistantSettings>(cx)
|
||||||
.default_open_ai_model
|
.default_open_ai_model
|
||||||
.clone();
|
.clone();
|
||||||
|
let model_name = model.full_name();
|
||||||
|
|
||||||
|
let prompt = cx.background().spawn(async move {
|
||||||
|
let snippets = snippets.await;
|
||||||
|
|
||||||
|
let language_name = language_name.as_deref();
|
||||||
|
generate_content_prompt(
|
||||||
|
user_prompt,
|
||||||
|
language_name,
|
||||||
|
&buffer,
|
||||||
|
range,
|
||||||
|
codegen_kind,
|
||||||
|
snippets,
|
||||||
|
model_name,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut messages = Vec::new();
|
||||||
if let Some(conversation) = conversation {
|
if let Some(conversation) = conversation {
|
||||||
let conversation = conversation.read(cx);
|
let conversation = conversation.read(cx);
|
||||||
let buffer = conversation.buffer.read(cx);
|
let buffer = conversation.buffer.read(cx);
|
||||||
|
@ -608,11 +716,6 @@ impl AssistantPanel {
|
||||||
model = conversation.model.clone();
|
model = conversation.model.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
let prompt = cx.background().spawn(async move {
|
|
||||||
let language_name = language_name.as_deref();
|
|
||||||
generate_content_prompt(user_prompt, language_name, &buffer, range, codegen_kind)
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let prompt = prompt.await;
|
let prompt = prompt.await;
|
||||||
|
|
||||||
|
@ -1514,12 +1617,14 @@ impl Conversation {
|
||||||
Role::Assistant => "assistant".into(),
|
Role::Assistant => "assistant".into(),
|
||||||
Role::System => "system".into(),
|
Role::System => "system".into(),
|
||||||
},
|
},
|
||||||
content: self
|
content: Some(
|
||||||
.buffer
|
self.buffer
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.text_for_range(message.offset_range)
|
.text_for_range(message.offset_range)
|
||||||
.collect(),
|
.collect(),
|
||||||
|
),
|
||||||
name: None,
|
name: None,
|
||||||
|
function_call: None,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
@ -2638,12 +2743,16 @@ enum InlineAssistantEvent {
|
||||||
Confirmed {
|
Confirmed {
|
||||||
prompt: String,
|
prompt: String,
|
||||||
include_conversation: bool,
|
include_conversation: bool,
|
||||||
|
retrieve_context: bool,
|
||||||
},
|
},
|
||||||
Canceled,
|
Canceled,
|
||||||
Dismissed,
|
Dismissed,
|
||||||
IncludeConversationToggled {
|
IncludeConversationToggled {
|
||||||
include_conversation: bool,
|
include_conversation: bool,
|
||||||
},
|
},
|
||||||
|
RetrieveContextToggled {
|
||||||
|
retrieve_context: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InlineAssistant {
|
struct InlineAssistant {
|
||||||
|
@ -2659,6 +2768,11 @@ struct InlineAssistant {
|
||||||
pending_prompt: String,
|
pending_prompt: String,
|
||||||
codegen: ModelHandle<Codegen>,
|
codegen: ModelHandle<Codegen>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
retrieve_context: bool,
|
||||||
|
semantic_index: Option<ModelHandle<SemanticIndex>>,
|
||||||
|
semantic_permissioned: Option<bool>,
|
||||||
|
project: WeakModelHandle<Project>,
|
||||||
|
maintain_rate_limit: Option<Task<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for InlineAssistant {
|
impl Entity for InlineAssistant {
|
||||||
|
@ -2675,51 +2789,65 @@ impl View for InlineAssistant {
|
||||||
let theme = theme::current(cx);
|
let theme = theme::current(cx);
|
||||||
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_children([Flex::row()
|
||||||
Flex::row()
|
.with_child(
|
||||||
.with_child(
|
Button::action(ToggleIncludeConversation)
|
||||||
Button::action(ToggleIncludeConversation)
|
.with_tooltip("Include Conversation", theme.tooltip.clone())
|
||||||
.with_tooltip("Include Conversation", theme.tooltip.clone())
|
.with_id(self.id)
|
||||||
|
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
|
||||||
|
.toggleable(self.include_conversation)
|
||||||
|
.with_style(theme.assistant.inline.include_conversation.clone())
|
||||||
|
.element()
|
||||||
|
.aligned(),
|
||||||
|
)
|
||||||
|
.with_children(if SemanticIndex::enabled(cx) {
|
||||||
|
Some(
|
||||||
|
Button::action(ToggleRetrieveContext)
|
||||||
|
.with_tooltip("Retrieve Context", theme.tooltip.clone())
|
||||||
.with_id(self.id)
|
.with_id(self.id)
|
||||||
.with_contents(theme::components::svg::Svg::new("icons/ai.svg"))
|
.with_contents(theme::components::svg::Svg::new(
|
||||||
.toggleable(self.include_conversation)
|
"icons/magnifying_glass.svg",
|
||||||
.with_style(theme.assistant.inline.include_conversation.clone())
|
))
|
||||||
|
.toggleable(self.retrieve_context)
|
||||||
|
.with_style(theme.assistant.inline.retrieve_context.clone())
|
||||||
.element()
|
.element()
|
||||||
.aligned(),
|
.aligned(),
|
||||||
)
|
)
|
||||||
.with_children(if let Some(error) = self.codegen.read(cx).error() {
|
} else {
|
||||||
Some(
|
None
|
||||||
Svg::new("icons/error.svg")
|
})
|
||||||
.with_color(theme.assistant.error_icon.color)
|
.with_children(if let Some(error) = self.codegen.read(cx).error() {
|
||||||
.constrained()
|
Some(
|
||||||
.with_width(theme.assistant.error_icon.width)
|
Svg::new("icons/error.svg")
|
||||||
.contained()
|
.with_color(theme.assistant.error_icon.color)
|
||||||
.with_style(theme.assistant.error_icon.container)
|
.constrained()
|
||||||
.with_tooltip::<ErrorIcon>(
|
.with_width(theme.assistant.error_icon.width)
|
||||||
self.id,
|
.contained()
|
||||||
error.to_string(),
|
.with_style(theme.assistant.error_icon.container)
|
||||||
None,
|
.with_tooltip::<ErrorIcon>(
|
||||||
theme.tooltip.clone(),
|
self.id,
|
||||||
cx,
|
error.to_string(),
|
||||||
)
|
None,
|
||||||
.aligned(),
|
theme.tooltip.clone(),
|
||||||
)
|
cx,
|
||||||
} else {
|
)
|
||||||
None
|
.aligned(),
|
||||||
})
|
)
|
||||||
.aligned()
|
} else {
|
||||||
.constrained()
|
None
|
||||||
.dynamically({
|
})
|
||||||
let measurements = self.measurements.clone();
|
.aligned()
|
||||||
move |constraint, _, _| {
|
.constrained()
|
||||||
let measurements = measurements.get();
|
.dynamically({
|
||||||
SizeConstraint {
|
let measurements = self.measurements.clone();
|
||||||
min: vec2f(measurements.gutter_width, constraint.min.y()),
|
move |constraint, _, _| {
|
||||||
max: vec2f(measurements.gutter_width, constraint.max.y()),
|
let measurements = measurements.get();
|
||||||
}
|
SizeConstraint {
|
||||||
|
min: vec2f(measurements.gutter_width, constraint.min.y()),
|
||||||
|
max: vec2f(measurements.gutter_width, constraint.max.y()),
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
)
|
})])
|
||||||
.with_child(Empty::new().constrained().dynamically({
|
.with_child(Empty::new().constrained().dynamically({
|
||||||
let measurements = self.measurements.clone();
|
let measurements = self.measurements.clone();
|
||||||
move |constraint, _, _| {
|
move |constraint, _, _| {
|
||||||
|
@ -2742,6 +2870,16 @@ impl View for InlineAssistant {
|
||||||
.left()
|
.left()
|
||||||
.flex(1., true),
|
.flex(1., true),
|
||||||
)
|
)
|
||||||
|
.with_children(if self.retrieve_context {
|
||||||
|
Some(
|
||||||
|
Flex::row()
|
||||||
|
.with_children(self.retrieve_context_status(cx))
|
||||||
|
.flex(1., true)
|
||||||
|
.aligned(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.assistant.inline.container)
|
.with_style(theme.assistant.inline.container)
|
||||||
.into_any()
|
.into_any()
|
||||||
|
@ -2767,6 +2905,9 @@ impl InlineAssistant {
|
||||||
codegen: ModelHandle<Codegen>,
|
codegen: ModelHandle<Codegen>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
|
retrieve_context: bool,
|
||||||
|
semantic_index: Option<ModelHandle<SemanticIndex>>,
|
||||||
|
project: ModelHandle<Project>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let prompt_editor = cx.add_view(|cx| {
|
let prompt_editor = cx.add_view(|cx| {
|
||||||
let mut editor = Editor::single_line(
|
let mut editor = Editor::single_line(
|
||||||
|
@ -2780,11 +2921,16 @@ impl InlineAssistant {
|
||||||
editor.set_placeholder_text(placeholder, cx);
|
editor.set_placeholder_text(placeholder, cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
let subscriptions = vec![
|
let mut subscriptions = vec![
|
||||||
cx.observe(&codegen, Self::handle_codegen_changed),
|
cx.observe(&codegen, Self::handle_codegen_changed),
|
||||||
cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
|
cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
|
||||||
];
|
];
|
||||||
Self {
|
|
||||||
|
if let Some(semantic_index) = semantic_index.clone() {
|
||||||
|
subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed));
|
||||||
|
}
|
||||||
|
|
||||||
|
let assistant = Self {
|
||||||
id,
|
id,
|
||||||
prompt_editor,
|
prompt_editor,
|
||||||
workspace,
|
workspace,
|
||||||
|
@ -2797,7 +2943,33 @@ impl InlineAssistant {
|
||||||
pending_prompt: String::new(),
|
pending_prompt: String::new(),
|
||||||
codegen,
|
codegen,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
|
retrieve_context,
|
||||||
|
semantic_permissioned: None,
|
||||||
|
semantic_index,
|
||||||
|
project: project.downgrade(),
|
||||||
|
maintain_rate_limit: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
assistant.index_project(cx).log_err();
|
||||||
|
|
||||||
|
assistant
|
||||||
|
}
|
||||||
|
|
||||||
|
fn semantic_permissioned(&self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
|
||||||
|
if let Some(value) = self.semantic_permissioned {
|
||||||
|
return Task::ready(Ok(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let Some(project) = self.project.upgrade(cx) else {
|
||||||
|
return Task::ready(Err(anyhow!("project was dropped")));
|
||||||
|
};
|
||||||
|
|
||||||
|
self.semantic_index
|
||||||
|
.as_ref()
|
||||||
|
.map(|semantic| {
|
||||||
|
semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
|
||||||
|
})
|
||||||
|
.unwrap_or(Task::ready(Ok(false)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_prompt_editor_events(
|
fn handle_prompt_editor_events(
|
||||||
|
@ -2812,6 +2984,37 @@ impl InlineAssistant {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn semantic_index_changed(
|
||||||
|
&mut self,
|
||||||
|
semantic_index: ModelHandle<SemanticIndex>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let Some(project) = self.project.upgrade(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = semantic_index.read(cx).status(&project);
|
||||||
|
match status {
|
||||||
|
SemanticIndexStatus::Indexing {
|
||||||
|
rate_limit_expiry: Some(_),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if self.maintain_rate_limit.is_none() {
|
||||||
|
self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move {
|
||||||
|
loop {
|
||||||
|
cx.background().timer(Duration::from_secs(1)).await;
|
||||||
|
this.update(&mut cx, |_, cx| cx.notify()).log_err();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.maintain_rate_limit = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_codegen_changed(&mut self, _: ModelHandle<Codegen>, cx: &mut ViewContext<Self>) {
|
fn handle_codegen_changed(&mut self, _: ModelHandle<Codegen>, cx: &mut ViewContext<Self>) {
|
||||||
let is_read_only = !self.codegen.read(cx).idle();
|
let is_read_only = !self.codegen.read(cx).idle();
|
||||||
self.prompt_editor.update(cx, |editor, cx| {
|
self.prompt_editor.update(cx, |editor, cx| {
|
||||||
|
@ -2861,12 +3064,241 @@ impl InlineAssistant {
|
||||||
cx.emit(InlineAssistantEvent::Confirmed {
|
cx.emit(InlineAssistantEvent::Confirmed {
|
||||||
prompt,
|
prompt,
|
||||||
include_conversation: self.include_conversation,
|
include_conversation: self.include_conversation,
|
||||||
|
retrieve_context: self.retrieve_context,
|
||||||
});
|
});
|
||||||
self.confirmed = true;
|
self.confirmed = true;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext<Self>) {
|
||||||
|
let semantic_permissioned = self.semantic_permissioned(cx);
|
||||||
|
|
||||||
|
let Some(project) = self.project.upgrade(cx) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let project_name = project
|
||||||
|
.read(cx)
|
||||||
|
.worktree_root_names(cx)
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join("/");
|
||||||
|
let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0;
|
||||||
|
let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name,
|
||||||
|
if is_plural {
|
||||||
|
"s"
|
||||||
|
} else {""});
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
// If Necessary prompt user
|
||||||
|
if !semantic_permissioned.await.unwrap_or(false) {
|
||||||
|
let mut answer = this.update(&mut cx, |_, cx| {
|
||||||
|
cx.prompt(
|
||||||
|
PromptLevel::Info,
|
||||||
|
prompt_text.as_str(),
|
||||||
|
&["Continue", "Cancel"],
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if answer.next().await == Some(0) {
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.semantic_permissioned = Some(true);
|
||||||
|
})?;
|
||||||
|
} else {
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If permissioned, update context appropriately
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.retrieve_context = !this.retrieve_context;
|
||||||
|
|
||||||
|
cx.emit(InlineAssistantEvent::RetrieveContextToggled {
|
||||||
|
retrieve_context: this.retrieve_context,
|
||||||
|
});
|
||||||
|
|
||||||
|
if this.retrieve_context {
|
||||||
|
this.index_project(cx).log_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
})?;
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index_project(&self, cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
|
||||||
|
let Some(project) = self.project.upgrade(cx) else {
|
||||||
|
return Err(anyhow!("project was dropped!"));
|
||||||
|
};
|
||||||
|
|
||||||
|
let semantic_permissioned = self.semantic_permissioned(cx);
|
||||||
|
if let Some(semantic_index) = SemanticIndex::global(cx) {
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
// This has to be updated to accomodate for semantic_permissions
|
||||||
|
if semantic_permissioned.await.unwrap_or(false) {
|
||||||
|
semantic_index
|
||||||
|
.update(&mut cx, |index, cx| index.index_project(project, cx))
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("project is not permissioned for semantic indexing"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn retrieve_context_status(
|
||||||
|
&self,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<AnyElement<InlineAssistant>> {
|
||||||
|
enum ContextStatusIcon {}
|
||||||
|
|
||||||
|
let Some(project) = self.project.upgrade(cx) else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(semantic_index) = SemanticIndex::global(cx) {
|
||||||
|
let status = semantic_index.update(cx, |index, _| index.status(&project));
|
||||||
|
let theme = theme::current(cx);
|
||||||
|
match status {
|
||||||
|
SemanticIndexStatus::NotAuthenticated {} => Some(
|
||||||
|
Svg::new("icons/error.svg")
|
||||||
|
.with_color(theme.assistant.error_icon.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.assistant.error_icon.width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.assistant.error_icon.container)
|
||||||
|
.with_tooltip::<ContextStatusIcon>(
|
||||||
|
self.id,
|
||||||
|
"Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.",
|
||||||
|
None,
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.into_any(),
|
||||||
|
),
|
||||||
|
SemanticIndexStatus::NotIndexed {} => Some(
|
||||||
|
Svg::new("icons/error.svg")
|
||||||
|
.with_color(theme.assistant.inline.context_status.error_icon.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.assistant.inline.context_status.error_icon.width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.assistant.inline.context_status.error_icon.container)
|
||||||
|
.with_tooltip::<ContextStatusIcon>(
|
||||||
|
self.id,
|
||||||
|
"Not Indexed",
|
||||||
|
None,
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.into_any(),
|
||||||
|
),
|
||||||
|
SemanticIndexStatus::Indexing {
|
||||||
|
remaining_files,
|
||||||
|
rate_limit_expiry,
|
||||||
|
} => {
|
||||||
|
|
||||||
|
let mut status_text = if remaining_files == 0 {
|
||||||
|
"Indexing...".to_string()
|
||||||
|
} else {
|
||||||
|
format!("Remaining files to index: {remaining_files}")
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(rate_limit_expiry) = rate_limit_expiry {
|
||||||
|
let remaining_seconds = rate_limit_expiry.duration_since(Instant::now());
|
||||||
|
if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 {
|
||||||
|
write!(
|
||||||
|
status_text,
|
||||||
|
" (rate limit expires in {}s)",
|
||||||
|
remaining_seconds.as_secs()
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(
|
||||||
|
Svg::new("icons/update.svg")
|
||||||
|
.with_color(theme.assistant.inline.context_status.in_progress_icon.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.assistant.inline.context_status.in_progress_icon.width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.assistant.inline.context_status.in_progress_icon.container)
|
||||||
|
.with_tooltip::<ContextStatusIcon>(
|
||||||
|
self.id,
|
||||||
|
status_text,
|
||||||
|
None,
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.into_any(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SemanticIndexStatus::Indexed {} => Some(
|
||||||
|
Svg::new("icons/check.svg")
|
||||||
|
.with_color(theme.assistant.inline.context_status.complete_icon.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.assistant.inline.context_status.complete_icon.width)
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.assistant.inline.context_status.complete_icon.container)
|
||||||
|
.with_tooltip::<ContextStatusIcon>(
|
||||||
|
self.id,
|
||||||
|
"Index up to date",
|
||||||
|
None,
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.aligned()
|
||||||
|
.into_any(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> String {
|
||||||
|
// let project = self.project.clone();
|
||||||
|
// if let Some(semantic_index) = self.semantic_index.clone() {
|
||||||
|
// let status = semantic_index.update(cx, |index, cx| index.status(&project));
|
||||||
|
// return match status {
|
||||||
|
// // This theoretically shouldnt be a valid code path
|
||||||
|
// // As the inline assistant cant be launched without an API key
|
||||||
|
// // We keep it here for safety
|
||||||
|
// semantic_index::SemanticIndexStatus::NotAuthenticated => {
|
||||||
|
// "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string()
|
||||||
|
// }
|
||||||
|
// semantic_index::SemanticIndexStatus::Indexed => {
|
||||||
|
// "Indexing Complete!".to_string()
|
||||||
|
// }
|
||||||
|
// semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => {
|
||||||
|
|
||||||
|
// let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}");
|
||||||
|
|
||||||
|
// if let Some(rate_limit_expiry) = rate_limit_expiry {
|
||||||
|
// let remaining_seconds =
|
||||||
|
// rate_limit_expiry.duration_since(Instant::now());
|
||||||
|
// if remaining_seconds > Duration::from_secs(0) {
|
||||||
|
// write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// status
|
||||||
|
// }
|
||||||
|
// semantic_index::SemanticIndexStatus::NotIndexed => {
|
||||||
|
// "Not Indexed for Context Retrieval".to_string()
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// "".to_string()
|
||||||
|
// }
|
||||||
|
|
||||||
fn toggle_include_conversation(
|
fn toggle_include_conversation(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &ToggleIncludeConversation,
|
_: &ToggleIncludeConversation,
|
||||||
|
@ -2929,6 +3361,7 @@ struct PendingInlineAssist {
|
||||||
inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
|
inline_assistant: Option<(BlockId, ViewHandle<InlineAssistant>)>,
|
||||||
codegen: ModelHandle<Codegen>,
|
codegen: ModelHandle<Codegen>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
|
project: WeakModelHandle<Project>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
|
||||||
|
|
|
@ -1,8 +1,60 @@
|
||||||
use crate::codegen::CodegenKind;
|
use crate::codegen::CodegenKind;
|
||||||
|
use gpui::AsyncAppContext;
|
||||||
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
use language::{BufferSnapshot, OffsetRangeExt, ToOffset};
|
||||||
|
use semantic_index::SearchResult;
|
||||||
use std::cmp::{self, Reverse};
|
use std::cmp::{self, Reverse};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tiktoken_rs::ChatCompletionRequestMessage;
|
||||||
|
|
||||||
|
pub struct PromptCodeSnippet {
|
||||||
|
path: Option<PathBuf>,
|
||||||
|
language_name: Option<String>,
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PromptCodeSnippet {
|
||||||
|
pub fn new(search_result: SearchResult, cx: &AsyncAppContext) -> Self {
|
||||||
|
let (content, language_name, file_path) =
|
||||||
|
search_result.buffer.read_with(cx, |buffer, _| {
|
||||||
|
let snapshot = buffer.snapshot();
|
||||||
|
let content = snapshot
|
||||||
|
.text_for_range(search_result.range.clone())
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let language_name = buffer
|
||||||
|
.language()
|
||||||
|
.and_then(|language| Some(language.name().to_string()));
|
||||||
|
|
||||||
|
let file_path = buffer
|
||||||
|
.file()
|
||||||
|
.and_then(|file| Some(file.path().to_path_buf()));
|
||||||
|
|
||||||
|
(content, language_name, file_path)
|
||||||
|
});
|
||||||
|
|
||||||
|
PromptCodeSnippet {
|
||||||
|
path: file_path,
|
||||||
|
language_name,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for PromptCodeSnippet {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let path = self
|
||||||
|
.path
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|path| Some(path.to_string_lossy().to_string()))
|
||||||
|
.unwrap_or("".to_string());
|
||||||
|
let language_name = self.language_name.clone().unwrap_or("".to_string());
|
||||||
|
let content = self.content.clone();
|
||||||
|
|
||||||
|
format!("The below code snippet may be relevant from file: {path}\n```{language_name}\n{content}\n```")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
fn summarize(buffer: &BufferSnapshot, selected_range: Range<impl ToOffset>) -> String {
|
||||||
|
@ -121,17 +173,25 @@ pub fn generate_content_prompt(
|
||||||
buffer: &BufferSnapshot,
|
buffer: &BufferSnapshot,
|
||||||
range: Range<impl ToOffset>,
|
range: Range<impl ToOffset>,
|
||||||
kind: CodegenKind,
|
kind: CodegenKind,
|
||||||
|
search_results: Vec<PromptCodeSnippet>,
|
||||||
|
model: &str,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
const MAXIMUM_SNIPPET_TOKEN_COUNT: usize = 500;
|
||||||
|
const RESERVED_TOKENS_FOR_GENERATION: usize = 1000;
|
||||||
|
|
||||||
|
let mut prompts = Vec::new();
|
||||||
let range = range.to_offset(buffer);
|
let range = range.to_offset(buffer);
|
||||||
let mut prompt = String::new();
|
|
||||||
|
|
||||||
// General Preamble
|
// General Preamble
|
||||||
if let Some(language_name) = language_name {
|
if let Some(language_name) = language_name {
|
||||||
writeln!(prompt, "You're an expert {language_name} engineer.\n").unwrap();
|
prompts.push(format!("You're an expert {language_name} engineer.\n"));
|
||||||
} else {
|
} else {
|
||||||
writeln!(prompt, "You're an expert engineer.\n").unwrap();
|
prompts.push("You're an expert engineer.\n".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Snippets
|
||||||
|
let mut snippet_position = prompts.len() - 1;
|
||||||
|
|
||||||
let mut content = String::new();
|
let mut content = String::new();
|
||||||
content.extend(buffer.text_for_range(0..range.start));
|
content.extend(buffer.text_for_range(0..range.start));
|
||||||
if range.start == range.end {
|
if range.start == range.end {
|
||||||
|
@ -145,59 +205,103 @@ pub fn generate_content_prompt(
|
||||||
}
|
}
|
||||||
content.extend(buffer.text_for_range(range.end..buffer.len()));
|
content.extend(buffer.text_for_range(range.end..buffer.len()));
|
||||||
|
|
||||||
writeln!(
|
prompts.push("The file you are currently working on has the following content:\n".to_string());
|
||||||
prompt,
|
|
||||||
"The file you are currently working on has the following content:"
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
if let Some(language_name) = language_name {
|
if let Some(language_name) = language_name {
|
||||||
let language_name = language_name.to_lowercase();
|
let language_name = language_name.to_lowercase();
|
||||||
writeln!(prompt, "```{language_name}\n{content}\n```").unwrap();
|
prompts.push(format!("```{language_name}\n{content}\n```"));
|
||||||
} else {
|
} else {
|
||||||
writeln!(prompt, "```\n{content}\n```").unwrap();
|
prompts.push(format!("```\n{content}\n```"));
|
||||||
}
|
}
|
||||||
|
|
||||||
match kind {
|
match kind {
|
||||||
CodegenKind::Generate { position: _ } => {
|
CodegenKind::Generate { position: _ } => {
|
||||||
writeln!(prompt, "In particular, the user's cursor is current on the '<|START|>' span in the above outline, with no text selected.").unwrap();
|
prompts.push("In particular, the user's cursor is currently on the '<|START|>' span in the above outline, with no text selected.".to_string());
|
||||||
writeln!(
|
prompts
|
||||||
prompt,
|
.push("Assume the cursor is located where the `<|START|` marker is.".to_string());
|
||||||
"Assume the cursor is located where the `<|START|` marker is."
|
prompts.push(
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
writeln!(
|
|
||||||
prompt,
|
|
||||||
"Text can't be replaced, so assume your answer will be inserted at the cursor."
|
"Text can't be replaced, so assume your answer will be inserted at the cursor."
|
||||||
)
|
.to_string(),
|
||||||
.unwrap();
|
);
|
||||||
writeln!(
|
prompts.push(format!(
|
||||||
prompt,
|
|
||||||
"Generate text based on the users prompt: {user_prompt}"
|
"Generate text based on the users prompt: {user_prompt}"
|
||||||
)
|
));
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
CodegenKind::Transform { range: _ } => {
|
CodegenKind::Transform { range: _ } => {
|
||||||
writeln!(prompt, "In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.").unwrap();
|
prompts.push("In particular, the user has selected a section of the text between the '<|START|' and '|END|>' spans.".to_string());
|
||||||
writeln!(
|
prompts.push(format!(
|
||||||
prompt,
|
"Modify the users code selected text based upon the users prompt: '{user_prompt}'"
|
||||||
"Modify the users code selected text based upon the users prompt: {user_prompt}"
|
));
|
||||||
)
|
prompts.push("You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file.".to_string());
|
||||||
.unwrap();
|
|
||||||
writeln!(
|
|
||||||
prompt,
|
|
||||||
"You MUST reply with only the adjusted code (within the '<|START|' and '|END|>' spans), not the entire file."
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(language_name) = language_name {
|
if let Some(language_name) = language_name {
|
||||||
writeln!(prompt, "Your answer MUST always be valid {language_name}").unwrap();
|
prompts.push(format!(
|
||||||
|
"Your answer MUST always and only be valid {language_name}"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
writeln!(prompt, "Always wrap your response in a Markdown codeblock").unwrap();
|
prompts.push("Never make remarks about the output.".to_string());
|
||||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
prompts.push("Do not return any text, except the generated code.".to_string());
|
||||||
|
prompts.push("Always wrap your code in a Markdown block".to_string());
|
||||||
|
|
||||||
prompt
|
let current_messages = [ChatCompletionRequestMessage {
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: Some(prompts.join("\n")),
|
||||||
|
function_call: None,
|
||||||
|
name: None,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let mut remaining_token_count = if let Ok(current_token_count) =
|
||||||
|
tiktoken_rs::num_tokens_from_messages(model, ¤t_messages)
|
||||||
|
{
|
||||||
|
let max_token_count = tiktoken_rs::model::get_context_size(model);
|
||||||
|
let intermediate_token_count = if max_token_count > current_token_count {
|
||||||
|
max_token_count - current_token_count
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
if intermediate_token_count < RESERVED_TOKENS_FOR_GENERATION {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
intermediate_token_count - RESERVED_TOKENS_FOR_GENERATION
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If tiktoken fails to count token count, assume we have no space remaining.
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
// - add repository name to snippet
|
||||||
|
// - add file path
|
||||||
|
// - add language
|
||||||
|
if let Ok(encoding) = tiktoken_rs::get_bpe_from_model(model) {
|
||||||
|
let mut template = "You are working inside a large repository, here are a few code snippets that may be useful";
|
||||||
|
|
||||||
|
for search_result in search_results {
|
||||||
|
let mut snippet_prompt = template.to_string();
|
||||||
|
let snippet = search_result.to_string();
|
||||||
|
writeln!(snippet_prompt, "```\n{snippet}\n```").unwrap();
|
||||||
|
|
||||||
|
let token_count = encoding
|
||||||
|
.encode_with_special_tokens(snippet_prompt.as_str())
|
||||||
|
.len();
|
||||||
|
if token_count <= remaining_token_count {
|
||||||
|
if token_count < MAXIMUM_SNIPPET_TOKEN_COUNT {
|
||||||
|
prompts.insert(snippet_position, snippet_prompt);
|
||||||
|
snippet_position += 1;
|
||||||
|
remaining_token_count -= token_count;
|
||||||
|
// If you have already added the template to the prompt, remove the template.
|
||||||
|
template = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prompts.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -9,7 +9,7 @@ use db::RELEASE_CHANNEL;
|
||||||
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt};
|
||||||
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle};
|
||||||
use rpc::{
|
use rpc::{
|
||||||
proto::{self, ChannelEdge, ChannelPermission},
|
proto::{self, ChannelEdge, ChannelPermission, ChannelRole, ChannelVisibility},
|
||||||
TypedEnvelope,
|
TypedEnvelope,
|
||||||
};
|
};
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
@ -49,6 +49,7 @@ pub type ChannelData = (Channel, ChannelPath);
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub id: ChannelId,
|
pub id: ChannelId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub visibility: proto::ChannelVisibility,
|
||||||
pub unseen_note_version: Option<(u64, clock::Global)>,
|
pub unseen_note_version: Option<(u64, clock::Global)>,
|
||||||
pub unseen_message_id: Option<u64>,
|
pub unseen_message_id: Option<u64>,
|
||||||
}
|
}
|
||||||
|
@ -79,7 +80,32 @@ pub struct ChannelPath(Arc<[ChannelId]>);
|
||||||
pub struct ChannelMembership {
|
pub struct ChannelMembership {
|
||||||
pub user: Arc<User>,
|
pub user: Arc<User>,
|
||||||
pub kind: proto::channel_member::Kind,
|
pub kind: proto::channel_member::Kind,
|
||||||
pub admin: bool,
|
pub role: proto::ChannelRole,
|
||||||
|
}
|
||||||
|
impl ChannelMembership {
|
||||||
|
pub fn sort_key(&self) -> MembershipSortKey {
|
||||||
|
MembershipSortKey {
|
||||||
|
role_order: match self.role {
|
||||||
|
proto::ChannelRole::Admin => 0,
|
||||||
|
proto::ChannelRole::Member => 1,
|
||||||
|
proto::ChannelRole::Banned => 2,
|
||||||
|
proto::ChannelRole::Guest => 3,
|
||||||
|
},
|
||||||
|
kind_order: match self.kind {
|
||||||
|
proto::channel_member::Kind::Member => 0,
|
||||||
|
proto::channel_member::Kind::AncestorMember => 1,
|
||||||
|
proto::channel_member::Kind::Invitee => 2,
|
||||||
|
},
|
||||||
|
username_order: self.user.github_login.as_str(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialOrd, Ord, PartialEq, Eq)]
|
||||||
|
pub struct MembershipSortKey<'a> {
|
||||||
|
role_order: u8,
|
||||||
|
kind_order: u8,
|
||||||
|
username_order: &'a str,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum ChannelEvent {
|
pub enum ChannelEvent {
|
||||||
|
@ -475,7 +501,7 @@ impl ChannelStore {
|
||||||
insert_edge: parent_edge,
|
insert_edge: parent_edge,
|
||||||
channel_permissions: vec![ChannelPermission {
|
channel_permissions: vec![ChannelPermission {
|
||||||
channel_id,
|
channel_id,
|
||||||
is_admin: true,
|
role: ChannelRole::Admin.into(),
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -547,11 +573,30 @@ impl ChannelStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_channel_visibility(
|
||||||
|
&mut self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
visibility: ChannelVisibility,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.clone();
|
||||||
|
cx.spawn(|_, _| async move {
|
||||||
|
let _ = client
|
||||||
|
.request(proto::SetChannelVisibility {
|
||||||
|
channel_id,
|
||||||
|
visibility: visibility.into(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn invite_member(
|
pub fn invite_member(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
admin: bool,
|
role: proto::ChannelRole,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||||
|
@ -565,7 +610,7 @@ impl ChannelStore {
|
||||||
.request(proto::InviteChannelMember {
|
.request(proto::InviteChannelMember {
|
||||||
channel_id,
|
channel_id,
|
||||||
user_id,
|
user_id,
|
||||||
admin,
|
role: role.into(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -609,11 +654,11 @@ impl ChannelStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_member_admin(
|
pub fn set_member_role(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
admin: bool,
|
role: proto::ChannelRole,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
if !self.outgoing_invites.insert((channel_id, user_id)) {
|
||||||
|
@ -624,10 +669,10 @@ impl ChannelStore {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
let result = client
|
let result = client
|
||||||
.request(proto::SetChannelMemberAdmin {
|
.request(proto::SetChannelMemberRole {
|
||||||
channel_id,
|
channel_id,
|
||||||
user_id,
|
user_id,
|
||||||
admin,
|
role: role.into(),
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -716,8 +761,8 @@ impl ChannelStore {
|
||||||
.filter_map(|(user, member)| {
|
.filter_map(|(user, member)| {
|
||||||
Some(ChannelMembership {
|
Some(ChannelMembership {
|
||||||
user,
|
user,
|
||||||
admin: member.admin,
|
role: member.role(),
|
||||||
kind: proto::channel_member::Kind::from_i32(member.kind)?,
|
kind: member.kind(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
|
@ -912,6 +957,7 @@ impl ChannelStore {
|
||||||
ix,
|
ix,
|
||||||
Arc::new(Channel {
|
Arc::new(Channel {
|
||||||
id: channel.id,
|
id: channel.id,
|
||||||
|
visibility: channel.visibility(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
unseen_note_version: None,
|
unseen_note_version: None,
|
||||||
unseen_message_id: None,
|
unseen_message_id: None,
|
||||||
|
@ -978,7 +1024,7 @@ impl ChannelStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
for permission in payload.channel_permissions {
|
for permission in payload.channel_permissions {
|
||||||
if permission.is_admin {
|
if permission.role() == proto::ChannelRole::Admin {
|
||||||
self.channels_with_admin_privileges
|
self.channels_with_admin_privileges
|
||||||
.insert(permission.channel_id);
|
.insert(permission.channel_id);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -123,12 +123,15 @@ impl<'a> ChannelPathsInsertGuard<'a> {
|
||||||
|
|
||||||
pub fn insert(&mut self, channel_proto: proto::Channel) {
|
pub fn insert(&mut self, channel_proto: proto::Channel) {
|
||||||
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) {
|
||||||
Arc::make_mut(existing_channel).name = channel_proto.name;
|
let existing_channel = Arc::make_mut(existing_channel);
|
||||||
|
existing_channel.visibility = channel_proto.visibility();
|
||||||
|
existing_channel.name = channel_proto.name;
|
||||||
} else {
|
} else {
|
||||||
self.channels_by_id.insert(
|
self.channels_by_id.insert(
|
||||||
channel_proto.id,
|
channel_proto.id,
|
||||||
Arc::new(Channel {
|
Arc::new(Channel {
|
||||||
id: channel_proto.id,
|
id: channel_proto.id,
|
||||||
|
visibility: channel_proto.visibility(),
|
||||||
name: channel_proto.name,
|
name: channel_proto.name,
|
||||||
unseen_note_version: None,
|
unseen_note_version: None,
|
||||||
unseen_message_id: None,
|
unseen_message_id: None,
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::channel_chat::ChannelChatEvent;
|
||||||
use super::*;
|
use super::*;
|
||||||
use client::{test::FakeServer, Client, UserStore};
|
use client::{test::FakeServer, Client, UserStore};
|
||||||
use gpui::{AppContext, ModelHandle, TestAppContext};
|
use gpui::{AppContext, ModelHandle, TestAppContext};
|
||||||
use rpc::proto;
|
use rpc::proto::{self};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use util::http::FakeHttpClient;
|
use util::http::FakeHttpClient;
|
||||||
|
|
||||||
|
@ -18,15 +18,17 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "a".to_string(),
|
name: "a".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
channel_permissions: vec![proto::ChannelPermission {
|
channel_permissions: vec![proto::ChannelPermission {
|
||||||
channel_id: 1,
|
channel_id: 1,
|
||||||
is_admin: true,
|
role: proto::ChannelRole::Admin.into(),
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -49,10 +51,12 @@ fn test_update_channels(cx: &mut AppContext) {
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 3,
|
id: 3,
|
||||||
name: "x".to_string(),
|
name: "x".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 4,
|
id: 4,
|
||||||
name: "y".to_string(),
|
name: "y".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
insert_edge: vec![
|
insert_edge: vec![
|
||||||
|
@ -92,14 +96,17 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 0,
|
id: 0,
|
||||||
name: "a".to_string(),
|
name: "a".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
},
|
},
|
||||||
proto::Channel {
|
proto::Channel {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: "c".to_string(),
|
name: "c".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
insert_edge: vec![
|
insert_edge: vec![
|
||||||
|
@ -114,7 +121,7 @@ fn test_dangling_channel_paths(cx: &mut AppContext) {
|
||||||
],
|
],
|
||||||
channel_permissions: vec![proto::ChannelPermission {
|
channel_permissions: vec![proto::ChannelPermission {
|
||||||
channel_id: 0,
|
channel_id: 0,
|
||||||
is_admin: true,
|
role: proto::ChannelRole::Admin.into(),
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -158,6 +165,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||||
channels: vec![proto::Channel {
|
channels: vec![proto::Channel {
|
||||||
id: channel_id,
|
id: channel_id,
|
||||||
name: "the-channel".to_string(),
|
name: "the-channel".to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
}],
|
}],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||||
default-run = "collab"
|
default-run = "collab"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "collab"
|
name = "collab"
|
||||||
version = "0.24.0"
|
version = "0.25.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -44,7 +44,7 @@ CREATE UNIQUE INDEX "index_rooms_on_channel_id" ON "rooms" ("channel_id");
|
||||||
|
|
||||||
CREATE TABLE "projects" (
|
CREATE TABLE "projects" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
"room_id" INTEGER REFERENCES rooms (id) NOT NULL,
|
"room_id" INTEGER REFERENCES rooms (id) ON DELETE CASCADE NOT NULL,
|
||||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||||
"host_connection_id" INTEGER,
|
"host_connection_id" INTEGER,
|
||||||
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
"host_connection_server_id" INTEGER REFERENCES servers (id) ON DELETE CASCADE,
|
||||||
|
@ -192,7 +192,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id");
|
||||||
CREATE TABLE "channels" (
|
CREATE TABLE "channels" (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
"name" VARCHAR NOT NULL,
|
"name" VARCHAR NOT NULL,
|
||||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"visibility" VARCHAR NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
CREATE TABLE IF NOT EXISTS "channel_chat_participants" (
|
||||||
|
@ -234,6 +235,7 @@ CREATE TABLE "channel_members" (
|
||||||
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
"channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE,
|
||||||
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
"user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
|
||||||
"admin" BOOLEAN NOT NULL DEFAULT false,
|
"admin" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"role" VARCHAR,
|
||||||
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
"accepted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
"updated_at" TIMESTAMP NOT NULL DEFAULT now
|
"updated_at" TIMESTAMP NOT NULL DEFAULT now
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
ALTER TABLE channel_members ADD COLUMN role TEXT;
|
||||||
|
UPDATE channel_members SET role = CASE WHEN admin THEN 'admin' ELSE 'member' END;
|
||||||
|
|
||||||
|
ALTER TABLE channels ADD COLUMN visibility TEXT NOT NULL DEFAULT 'members';
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
ALTER TABLE projects
|
||||||
|
DROP CONSTRAINT projects_room_id_fkey,
|
||||||
|
ADD CONSTRAINT projects_room_id_fkey
|
||||||
|
FOREIGN KEY (room_id)
|
||||||
|
REFERENCES rooms (id)
|
||||||
|
ON DELETE CASCADE;
|
|
@ -432,6 +432,7 @@ pub struct NewUserResult {
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub id: ChannelId,
|
pub id: ChannelId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub visibility: ChannelVisibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
|
use rpc::proto;
|
||||||
use sea_orm::{entity::prelude::*, DbErr};
|
use sea_orm::{entity::prelude::*, DbErr};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -82,3 +83,101 @@ id_type!(ChannelBufferCollaboratorId);
|
||||||
id_type!(FlagId);
|
id_type!(FlagId);
|
||||||
id_type!(NotificationId);
|
id_type!(NotificationId);
|
||||||
id_type!(NotificationKindId);
|
id_type!(NotificationKindId);
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default)]
|
||||||
|
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||||
|
pub enum ChannelRole {
|
||||||
|
#[sea_orm(string_value = "admin")]
|
||||||
|
Admin,
|
||||||
|
#[sea_orm(string_value = "member")]
|
||||||
|
#[default]
|
||||||
|
Member,
|
||||||
|
#[sea_orm(string_value = "guest")]
|
||||||
|
Guest,
|
||||||
|
#[sea_orm(string_value = "banned")]
|
||||||
|
Banned,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChannelRole {
|
||||||
|
pub fn should_override(&self, other: Self) -> bool {
|
||||||
|
use ChannelRole::*;
|
||||||
|
match self {
|
||||||
|
Admin => matches!(other, Member | Banned | Guest),
|
||||||
|
Member => matches!(other, Banned | Guest),
|
||||||
|
Banned => matches!(other, Guest),
|
||||||
|
Guest => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn max(&self, other: Self) -> Self {
|
||||||
|
if self.should_override(other) {
|
||||||
|
*self
|
||||||
|
} else {
|
||||||
|
other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<proto::ChannelRole> for ChannelRole {
|
||||||
|
fn from(value: proto::ChannelRole) -> Self {
|
||||||
|
match value {
|
||||||
|
proto::ChannelRole::Admin => ChannelRole::Admin,
|
||||||
|
proto::ChannelRole::Member => ChannelRole::Member,
|
||||||
|
proto::ChannelRole::Guest => ChannelRole::Guest,
|
||||||
|
proto::ChannelRole::Banned => ChannelRole::Banned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<proto::ChannelRole> for ChannelRole {
|
||||||
|
fn into(self) -> proto::ChannelRole {
|
||||||
|
match self {
|
||||||
|
ChannelRole::Admin => proto::ChannelRole::Admin,
|
||||||
|
ChannelRole::Member => proto::ChannelRole::Member,
|
||||||
|
ChannelRole::Guest => proto::ChannelRole::Guest,
|
||||||
|
ChannelRole::Banned => proto::ChannelRole::Banned,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<i32> for ChannelRole {
|
||||||
|
fn into(self) -> i32 {
|
||||||
|
let proto: proto::ChannelRole = self.into();
|
||||||
|
proto.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Copy, Clone, Debug, EnumIter, DeriveActiveEnum, Default, Hash)]
|
||||||
|
#[sea_orm(rs_type = "String", db_type = "String(None)")]
|
||||||
|
pub enum ChannelVisibility {
|
||||||
|
#[sea_orm(string_value = "public")]
|
||||||
|
Public,
|
||||||
|
#[sea_orm(string_value = "members")]
|
||||||
|
#[default]
|
||||||
|
Members,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<proto::ChannelVisibility> for ChannelVisibility {
|
||||||
|
fn from(value: proto::ChannelVisibility) -> Self {
|
||||||
|
match value {
|
||||||
|
proto::ChannelVisibility::Public => ChannelVisibility::Public,
|
||||||
|
proto::ChannelVisibility::Members => ChannelVisibility::Members,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<proto::ChannelVisibility> for ChannelVisibility {
|
||||||
|
fn into(self) -> proto::ChannelVisibility {
|
||||||
|
match self {
|
||||||
|
ChannelVisibility::Public => proto::ChannelVisibility::Public,
|
||||||
|
ChannelVisibility::Members => proto::ChannelVisibility::Members,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<i32> for ChannelVisibility {
|
||||||
|
fn into(self) -> i32 {
|
||||||
|
let proto: proto::ChannelVisibility = self.into();
|
||||||
|
proto.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -482,7 +482,9 @@ impl Database {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
channel_members = self
|
||||||
|
.get_channel_participants_internal(channel_id, &*tx)
|
||||||
|
.await?;
|
||||||
let collaborators = self
|
let collaborators = self
|
||||||
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
.get_channel_buffer_collaborators_internal(channel_id, &*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -233,7 +233,9 @@ impl Database {
|
||||||
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
|
self.observe_channel_message_internal(channel_id, user_id, message_id, &*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut channel_members = self.get_channel_members_internal(channel_id, &*tx).await?;
|
let mut channel_members = self
|
||||||
|
.get_channel_participants_internal(channel_id, &*tx)
|
||||||
|
.await?;
|
||||||
channel_members.retain(|member| !participant_user_ids.contains(member));
|
channel_members.retain(|member| !participant_user_ids.contains(member));
|
||||||
|
|
||||||
Ok((message_id, participant_connection_ids, channel_members))
|
Ok((message_id, participant_connection_ids, channel_members))
|
||||||
|
@ -386,8 +388,22 @@ impl Database {
|
||||||
.filter(channel_message::Column::SenderId.eq(user_id))
|
.filter(channel_message::Column::SenderId.eq(user_id))
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if result.rows_affected == 0 {
|
if result.rows_affected == 0 {
|
||||||
Err(anyhow!("no such message"))?;
|
if self
|
||||||
|
.check_user_is_channel_admin(channel_id, user_id, &*tx)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
let result = channel_message::Entity::delete_by_id(message_id)
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
if result.rows_affected == 0 {
|
||||||
|
Err(anyhow!("no such message"))?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("operation could not be completed"))?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(participant_connection_ids)
|
Ok(participant_connection_ids)
|
||||||
|
|
|
@ -53,7 +53,9 @@ impl Database {
|
||||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||||
let channel_members;
|
let channel_members;
|
||||||
if let Some(channel_id) = channel_id {
|
if let Some(channel_id) = channel_id {
|
||||||
channel_members = self.get_channel_members_internal(channel_id, &tx).await?;
|
channel_members = self
|
||||||
|
.get_channel_participants_internal(channel_id, &tx)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
channel_members = Vec::new();
|
channel_members = Vec::new();
|
||||||
|
|
||||||
|
@ -298,98 +300,139 @@ impl Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
if channel_id.is_some() {
|
||||||
enum QueryParticipantIndices {
|
Err(anyhow!("tried to join channel call directly"))?
|
||||||
ParticipantIndex,
|
|
||||||
}
|
}
|
||||||
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
|
||||||
.filter(
|
let participant_index = self
|
||||||
room_participant::Column::RoomId
|
.get_next_participant_index_internal(room_id, &*tx)
|
||||||
.eq(room_id)
|
|
||||||
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
|
||||||
)
|
|
||||||
.select_only()
|
|
||||||
.column(room_participant::Column::ParticipantIndex)
|
|
||||||
.into_values::<_, QueryParticipantIndices>()
|
|
||||||
.all(&*tx)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut participant_index = 0;
|
let result = room_participant::Entity::update_many()
|
||||||
while existing_participant_indices.contains(&participant_index) {
|
.filter(
|
||||||
participant_index += 1;
|
Condition::all()
|
||||||
}
|
.add(room_participant::Column::RoomId.eq(room_id))
|
||||||
|
.add(room_participant::Column::UserId.eq(user_id))
|
||||||
if let Some(channel_id) = channel_id {
|
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
||||||
self.check_user_is_channel_member(channel_id, user_id, &*tx)
|
)
|
||||||
.await?;
|
.set(room_participant::ActiveModel {
|
||||||
|
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||||
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
|
||||||
room_id: ActiveValue::set(room_id),
|
|
||||||
user_id: ActiveValue::set(user_id),
|
|
||||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
connection.owner_id as i32,
|
connection.owner_id as i32,
|
||||||
))),
|
))),
|
||||||
answering_connection_lost: ActiveValue::set(false),
|
answering_connection_lost: ActiveValue::set(false),
|
||||||
calling_user_id: ActiveValue::set(user_id),
|
|
||||||
calling_connection_id: ActiveValue::set(connection.id as i32),
|
|
||||||
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
|
||||||
connection.owner_id as i32,
|
|
||||||
))),
|
|
||||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}])
|
})
|
||||||
.on_conflict(
|
|
||||||
OnConflict::columns([room_participant::Column::UserId])
|
|
||||||
.update_columns([
|
|
||||||
room_participant::Column::AnsweringConnectionId,
|
|
||||||
room_participant::Column::AnsweringConnectionServerId,
|
|
||||||
room_participant::Column::AnsweringConnectionLost,
|
|
||||||
room_participant::Column::ParticipantIndex,
|
|
||||||
])
|
|
||||||
.to_owned(),
|
|
||||||
)
|
|
||||||
.exec(&*tx)
|
.exec(&*tx)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
if result.rows_affected == 0 {
|
||||||
let result = room_participant::Entity::update_many()
|
Err(anyhow!("room does not exist or was already joined"))?;
|
||||||
.filter(
|
|
||||||
Condition::all()
|
|
||||||
.add(room_participant::Column::RoomId.eq(room_id))
|
|
||||||
.add(room_participant::Column::UserId.eq(user_id))
|
|
||||||
.add(room_participant::Column::AnsweringConnectionId.is_null()),
|
|
||||||
)
|
|
||||||
.set(room_participant::ActiveModel {
|
|
||||||
participant_index: ActiveValue::Set(Some(participant_index)),
|
|
||||||
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
|
||||||
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
|
||||||
connection.owner_id as i32,
|
|
||||||
))),
|
|
||||||
answering_connection_lost: ActiveValue::set(false),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.exec(&*tx)
|
|
||||||
.await?;
|
|
||||||
if result.rows_affected == 0 {
|
|
||||||
Err(anyhow!("room does not exist or was already joined"))?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let room = self.get_room(room_id, &tx).await?;
|
let room = self.get_room(room_id, &tx).await?;
|
||||||
let channel_members = if let Some(channel_id) = channel_id {
|
|
||||||
self.get_channel_members_internal(channel_id, &tx).await?
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
Ok(JoinRoom {
|
Ok(JoinRoom {
|
||||||
room,
|
room,
|
||||||
channel_id,
|
channel_id: None,
|
||||||
channel_members,
|
channel_members: vec![],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_next_participant_index_internal(
|
||||||
|
&self,
|
||||||
|
room_id: RoomId,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<i32> {
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
|
||||||
|
enum QueryParticipantIndices {
|
||||||
|
ParticipantIndex,
|
||||||
|
}
|
||||||
|
let existing_participant_indices: Vec<i32> = room_participant::Entity::find()
|
||||||
|
.filter(
|
||||||
|
room_participant::Column::RoomId
|
||||||
|
.eq(room_id)
|
||||||
|
.and(room_participant::Column::ParticipantIndex.is_not_null()),
|
||||||
|
)
|
||||||
|
.select_only()
|
||||||
|
.column(room_participant::Column::ParticipantIndex)
|
||||||
|
.into_values::<_, QueryParticipantIndices>()
|
||||||
|
.all(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut participant_index = 0;
|
||||||
|
while existing_participant_indices.contains(&participant_index) {
|
||||||
|
participant_index += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(participant_index)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn channel_id_for_room(&self, room_id: RoomId) -> Result<Option<ChannelId>> {
|
||||||
|
self.transaction(|tx| async move {
|
||||||
|
let room: Option<room::Model> = room::Entity::find()
|
||||||
|
.filter(room::Column::Id.eq(room_id))
|
||||||
|
.one(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(room.and_then(|room| room.channel_id))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn join_channel_room_internal(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
room_id: RoomId,
|
||||||
|
user_id: UserId,
|
||||||
|
connection: ConnectionId,
|
||||||
|
tx: &DatabaseTransaction,
|
||||||
|
) -> Result<JoinRoom> {
|
||||||
|
let participant_index = self
|
||||||
|
.get_next_participant_index_internal(room_id, &*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
room_participant::Entity::insert_many([room_participant::ActiveModel {
|
||||||
|
room_id: ActiveValue::set(room_id),
|
||||||
|
user_id: ActiveValue::set(user_id),
|
||||||
|
answering_connection_id: ActiveValue::set(Some(connection.id as i32)),
|
||||||
|
answering_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
|
connection.owner_id as i32,
|
||||||
|
))),
|
||||||
|
answering_connection_lost: ActiveValue::set(false),
|
||||||
|
calling_user_id: ActiveValue::set(user_id),
|
||||||
|
calling_connection_id: ActiveValue::set(connection.id as i32),
|
||||||
|
calling_connection_server_id: ActiveValue::set(Some(ServerId(
|
||||||
|
connection.owner_id as i32,
|
||||||
|
))),
|
||||||
|
participant_index: ActiveValue::Set(Some(participant_index)),
|
||||||
|
..Default::default()
|
||||||
|
}])
|
||||||
|
.on_conflict(
|
||||||
|
OnConflict::columns([room_participant::Column::UserId])
|
||||||
|
.update_columns([
|
||||||
|
room_participant::Column::AnsweringConnectionId,
|
||||||
|
room_participant::Column::AnsweringConnectionServerId,
|
||||||
|
room_participant::Column::AnsweringConnectionLost,
|
||||||
|
room_participant::Column::ParticipantIndex,
|
||||||
|
])
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(&*tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let room = self.get_room(room_id, &tx).await?;
|
||||||
|
let channel_members = self
|
||||||
|
.get_channel_participants_internal(channel_id, &tx)
|
||||||
|
.await?;
|
||||||
|
Ok(JoinRoom {
|
||||||
|
room,
|
||||||
|
channel_id: Some(channel_id),
|
||||||
|
channel_members,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn rejoin_room(
|
pub async fn rejoin_room(
|
||||||
&self,
|
&self,
|
||||||
rejoin_room: proto::RejoinRoom,
|
rejoin_room: proto::RejoinRoom,
|
||||||
|
@ -681,7 +724,8 @@ impl Database {
|
||||||
|
|
||||||
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
let (channel_id, room) = self.get_channel_room(room_id, &tx).await?;
|
||||||
let channel_members = if let Some(channel_id) = channel_id {
|
let channel_members = if let Some(channel_id) = channel_id {
|
||||||
self.get_channel_members_internal(channel_id, &tx).await?
|
self.get_channel_participants_internal(channel_id, &tx)
|
||||||
|
.await?
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
@ -839,7 +883,8 @@ impl Database {
|
||||||
};
|
};
|
||||||
|
|
||||||
let channel_members = if let Some(channel_id) = channel_id {
|
let channel_members = if let Some(channel_id) = channel_id {
|
||||||
self.get_channel_members_internal(channel_id, &tx).await?
|
self.get_channel_participants_internal(channel_id, &tx)
|
||||||
|
.await?
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::db::ChannelId;
|
use crate::db::{ChannelId, ChannelVisibility};
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
||||||
|
@ -7,6 +7,7 @@ pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
pub id: ChannelId,
|
pub id: ChannelId,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub visibility: ChannelVisibility,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::db::{channel_member, ChannelId, ChannelMemberId, UserId};
|
use crate::db::{channel_member, ChannelId, ChannelMemberId, ChannelRole, UserId};
|
||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)]
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||||
#[sea_orm(table_name = "channel_members")]
|
#[sea_orm(table_name = "channel_members")]
|
||||||
pub struct Model {
|
pub struct Model {
|
||||||
#[sea_orm(primary_key)]
|
#[sea_orm(primary_key)]
|
||||||
|
@ -9,7 +9,7 @@ pub struct Model {
|
||||||
pub channel_id: ChannelId,
|
pub channel_id: ChannelId,
|
||||||
pub user_id: UserId,
|
pub user_id: UserId,
|
||||||
pub accepted: bool,
|
pub accepted: bool,
|
||||||
pub admin: bool,
|
pub role: ChannelRole,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActiveModelBehavior for ActiveModel {}
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
|
|
|
@ -161,6 +161,7 @@ fn graph(channels: &[(ChannelId, &'static str)], edges: &[(ChannelId, ChannelId)
|
||||||
graph.channels.push(Channel {
|
graph.channels.push(Channel {
|
||||||
id: *id,
|
id: *id,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
visibility: ChannelVisibility::Members,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ async fn test_channel_buffers(db: &Arc<Database>) {
|
||||||
|
|
||||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||||
|
|
||||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ async fn test_channel_buffers_last_operations(db: &Database) {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
db.invite_channel_member(channel, observer_id, user_id, false)
|
db.invite_channel_member(channel, observer_id, user_id, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.respond_to_channel_invite(channel, observer_id, true)
|
db.respond_to_channel_invite(channel, observer_id, true)
|
||||||
|
|
|
@ -8,11 +8,14 @@ use crate::{
|
||||||
db::{
|
db::{
|
||||||
queries::channels::ChannelGraph,
|
queries::channels::ChannelGraph,
|
||||||
tests::{graph, TEST_RELEASE_CHANNEL},
|
tests::{graph, TEST_RELEASE_CHANNEL},
|
||||||
ChannelId, Database, NewUserParams,
|
ChannelId, ChannelRole, Database, NewUserParams, RoomId, UserId,
|
||||||
},
|
},
|
||||||
test_both_dbs,
|
test_both_dbs,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::{
|
||||||
|
atomic::{AtomicI32, Ordering},
|
||||||
|
Arc,
|
||||||
|
};
|
||||||
|
|
||||||
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite);
|
||||||
|
|
||||||
|
@ -46,9 +49,9 @@ async fn test_channels(db: &Arc<Database>) {
|
||||||
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
let zed_id = db.create_root_channel("zed", a_id).await.unwrap();
|
||||||
|
|
||||||
// Make sure that people cannot read channels they haven't been invited to
|
// Make sure that people cannot read channels they haven't been invited to
|
||||||
assert!(db.get_channel(zed_id, b_id).await.unwrap().is_none());
|
assert!(db.get_channel(zed_id, b_id).await.is_err());
|
||||||
|
|
||||||
db.invite_channel_member(zed_id, b_id, a_id, false)
|
db.invite_channel_member(zed_id, b_id, a_id, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -123,9 +126,13 @@ async fn test_channels(db: &Arc<Database>) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update member permissions
|
// Update member permissions
|
||||||
let set_subchannel_admin = db.set_channel_member_admin(crdb_id, a_id, b_id, true).await;
|
let set_subchannel_admin = db
|
||||||
|
.set_channel_member_role(crdb_id, a_id, b_id, ChannelRole::Admin)
|
||||||
|
.await;
|
||||||
assert!(set_subchannel_admin.is_err());
|
assert!(set_subchannel_admin.is_err());
|
||||||
let set_channel_admin = db.set_channel_member_admin(zed_id, a_id, b_id, true).await;
|
let set_channel_admin = db
|
||||||
|
.set_channel_member_role(zed_id, a_id, b_id, ChannelRole::Admin)
|
||||||
|
.await;
|
||||||
assert!(set_channel_admin.is_ok());
|
assert!(set_channel_admin.is_ok());
|
||||||
|
|
||||||
let result = db.get_channels_for_user(b_id).await.unwrap();
|
let result = db.get_channels_for_user(b_id).await.unwrap();
|
||||||
|
@ -148,7 +155,7 @@ async fn test_channels(db: &Arc<Database>) {
|
||||||
|
|
||||||
// Remove a single channel
|
// Remove a single channel
|
||||||
db.delete_channel(crdb_id, a_id).await.unwrap();
|
db.delete_channel(crdb_id, a_id).await.unwrap();
|
||||||
assert!(db.get_channel(crdb_id, a_id).await.unwrap().is_none());
|
assert!(db.get_channel(crdb_id, a_id).await.is_err());
|
||||||
|
|
||||||
// Remove a channel tree
|
// Remove a channel tree
|
||||||
let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
|
let (mut channel_ids, user_ids) = db.delete_channel(rust_id, a_id).await.unwrap();
|
||||||
|
@ -156,9 +163,9 @@ async fn test_channels(db: &Arc<Database>) {
|
||||||
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
|
assert_eq!(channel_ids, &[rust_id, cargo_id, cargo_ra_id]);
|
||||||
assert_eq!(user_ids, &[a_id]);
|
assert_eq!(user_ids, &[a_id]);
|
||||||
|
|
||||||
assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none());
|
assert!(db.get_channel(rust_id, a_id).await.is_err());
|
||||||
assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none());
|
assert!(db.get_channel(cargo_id, a_id).await.is_err());
|
||||||
assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none());
|
assert!(db.get_channel(cargo_ra_id, a_id).await.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
test_both_dbs!(
|
test_both_dbs!(
|
||||||
|
@ -196,15 +203,11 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||||
.user_id;
|
.user_id;
|
||||||
|
|
||||||
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
let channel_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||||
let room_1 = db
|
|
||||||
.get_or_create_channel_room(channel_1, "1", TEST_RELEASE_CHANNEL)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// can join a room with membership to its channel
|
// can join a room with membership to its channel
|
||||||
let joined_room = db
|
let (joined_room, _) = db
|
||||||
.join_room(
|
.join_channel(
|
||||||
room_1,
|
channel_1,
|
||||||
user_1,
|
user_1,
|
||||||
ConnectionId { owner_id, id: 1 },
|
ConnectionId { owner_id, id: 1 },
|
||||||
TEST_RELEASE_CHANNEL,
|
TEST_RELEASE_CHANNEL,
|
||||||
|
@ -213,11 +216,12 @@ async fn test_joining_channels(db: &Arc<Database>) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(joined_room.room.participants.len(), 1);
|
assert_eq!(joined_room.room.participants.len(), 1);
|
||||||
|
|
||||||
|
let room_id = RoomId::from_proto(joined_room.room.id);
|
||||||
drop(joined_room);
|
drop(joined_room);
|
||||||
// cannot join a room without membership to its channel
|
// cannot join a room without membership to its channel
|
||||||
assert!(db
|
assert!(db
|
||||||
.join_room(
|
.join_room(
|
||||||
room_1,
|
room_id,
|
||||||
user_2,
|
user_2,
|
||||||
ConnectionId { owner_id, id: 1 },
|
ConnectionId { owner_id, id: 1 },
|
||||||
TEST_RELEASE_CHANNEL
|
TEST_RELEASE_CHANNEL
|
||||||
|
@ -235,55 +239,21 @@ test_both_dbs!(
|
||||||
async fn test_channel_invites(db: &Arc<Database>) {
|
async fn test_channel_invites(db: &Arc<Database>) {
|
||||||
db.create_server("test").await.unwrap();
|
db.create_server("test").await.unwrap();
|
||||||
|
|
||||||
let user_1 = db
|
let user_1 = new_test_user(db, "user1@example.com").await;
|
||||||
.create_user(
|
let user_2 = new_test_user(db, "user2@example.com").await;
|
||||||
"user1@example.com",
|
let user_3 = new_test_user(db, "user3@example.com").await;
|
||||||
false,
|
|
||||||
NewUserParams {
|
|
||||||
github_login: "user1".into(),
|
|
||||||
github_user_id: 5,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.user_id;
|
|
||||||
let user_2 = db
|
|
||||||
.create_user(
|
|
||||||
"user2@example.com",
|
|
||||||
false,
|
|
||||||
NewUserParams {
|
|
||||||
github_login: "user2".into(),
|
|
||||||
github_user_id: 6,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.user_id;
|
|
||||||
|
|
||||||
let user_3 = db
|
|
||||||
.create_user(
|
|
||||||
"user3@example.com",
|
|
||||||
false,
|
|
||||||
NewUserParams {
|
|
||||||
github_login: "user3".into(),
|
|
||||||
github_user_id: 7,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.user_id;
|
|
||||||
|
|
||||||
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
let channel_1_1 = db.create_root_channel("channel_1", user_1).await.unwrap();
|
||||||
|
|
||||||
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
|
let channel_1_2 = db.create_root_channel("channel_2", user_1).await.unwrap();
|
||||||
|
|
||||||
db.invite_channel_member(channel_1_1, user_2, user_1, false)
|
db.invite_channel_member(channel_1_1, user_2, user_1, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.invite_channel_member(channel_1_2, user_2, user_1, false)
|
db.invite_channel_member(channel_1_2, user_2, user_1, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.invite_channel_member(channel_1_1, user_3, user_1, true)
|
db.invite_channel_member(channel_1_1, user_3, user_1, ChannelRole::Admin)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -307,27 +277,29 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||||
|
|
||||||
assert_eq!(user_3_invites, &[channel_1_1]);
|
assert_eq!(user_3_invites, &[channel_1_1]);
|
||||||
|
|
||||||
let members = db
|
let mut members = db
|
||||||
.get_channel_member_details(channel_1_1, user_1)
|
.get_channel_participant_details(channel_1_1, user_1)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
members.sort_by_key(|member| member.user_id);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
members,
|
members,
|
||||||
&[
|
&[
|
||||||
proto::ChannelMember {
|
proto::ChannelMember {
|
||||||
user_id: user_1.to_proto(),
|
user_id: user_1.to_proto(),
|
||||||
kind: proto::channel_member::Kind::Member.into(),
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
admin: true,
|
role: proto::ChannelRole::Admin.into(),
|
||||||
},
|
},
|
||||||
proto::ChannelMember {
|
proto::ChannelMember {
|
||||||
user_id: user_2.to_proto(),
|
user_id: user_2.to_proto(),
|
||||||
kind: proto::channel_member::Kind::Invitee.into(),
|
kind: proto::channel_member::Kind::Invitee.into(),
|
||||||
admin: false,
|
role: proto::ChannelRole::Member.into(),
|
||||||
},
|
},
|
||||||
proto::ChannelMember {
|
proto::ChannelMember {
|
||||||
user_id: user_3.to_proto(),
|
user_id: user_3.to_proto(),
|
||||||
kind: proto::channel_member::Kind::Invitee.into(),
|
kind: proto::channel_member::Kind::Invitee.into(),
|
||||||
admin: true,
|
role: proto::ChannelRole::Admin.into(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -342,7 +314,7 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let members = db
|
let members = db
|
||||||
.get_channel_member_details(channel_1_3, user_1)
|
.get_channel_participant_details(channel_1_3, user_1)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -351,12 +323,12 @@ async fn test_channel_invites(db: &Arc<Database>) {
|
||||||
proto::ChannelMember {
|
proto::ChannelMember {
|
||||||
user_id: user_1.to_proto(),
|
user_id: user_1.to_proto(),
|
||||||
kind: proto::channel_member::Kind::Member.into(),
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
admin: true,
|
role: proto::ChannelRole::Admin.into(),
|
||||||
},
|
},
|
||||||
proto::ChannelMember {
|
proto::ChannelMember {
|
||||||
user_id: user_2.to_proto(),
|
user_id: user_2.to_proto(),
|
||||||
kind: proto::channel_member::Kind::AncestorMember.into(),
|
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||||
admin: false,
|
role: proto::ChannelRole::Member.into(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -405,11 +377,7 @@ async fn test_channel_renames(db: &Arc<Database>) {
|
||||||
|
|
||||||
let zed_archive_id = zed_id;
|
let zed_archive_id = zed_id;
|
||||||
|
|
||||||
let (channel, _) = db
|
let channel = db.get_channel(zed_archive_id, user_1).await.unwrap();
|
||||||
.get_channel(zed_archive_id, user_1)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(channel.name, "zed-archive");
|
assert_eq!(channel.name, "zed-archive");
|
||||||
|
|
||||||
let non_permissioned_rename = db
|
let non_permissioned_rename = db
|
||||||
|
@ -835,6 +803,284 @@ async fn test_db_channel_moving_bugs(db: &Arc<Database>) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test_both_dbs!(
|
||||||
|
test_user_is_channel_participant,
|
||||||
|
test_user_is_channel_participant_postgres,
|
||||||
|
test_user_is_channel_participant_sqlite
|
||||||
|
);
|
||||||
|
|
||||||
|
async fn test_user_is_channel_participant(db: &Arc<Database>) {
|
||||||
|
let admin = new_test_user(db, "admin@example.com").await;
|
||||||
|
let member = new_test_user(db, "member@example.com").await;
|
||||||
|
let guest = new_test_user(db, "guest@example.com").await;
|
||||||
|
|
||||||
|
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||||
|
let active_channel = db
|
||||||
|
.create_channel("active", Some(zed_channel), admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let vim_channel = db
|
||||||
|
.create_channel("vim", Some(active_channel), admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.invite_channel_member(active_channel, member, admin, ChannelRole::Member)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.invite_channel_member(vim_channel, guest, admin, ChannelRole::Guest)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.respond_to_channel_invite(active_channel, member, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.transaction(|tx| async move {
|
||||||
|
db.check_user_is_channel_participant(vim_channel, admin, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
db.transaction(|tx| async move {
|
||||||
|
db.check_user_is_channel_participant(vim_channel, member, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut members = db
|
||||||
|
.get_channel_participant_details(vim_channel, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
members.sort_by_key(|member| member.user_id);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
members,
|
||||||
|
&[
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: admin.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
|
role: proto::ChannelRole::Admin.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: member.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||||
|
role: proto::ChannelRole::Member.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: guest.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Invitee.into(),
|
||||||
|
role: proto::ChannelRole::Guest.into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
db.respond_to_channel_invite(vim_channel, guest, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.transaction(|tx| async move {
|
||||||
|
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||||
|
assert_dag(channels, &[(vim_channel, None)]);
|
||||||
|
let channels = db.get_channels_for_user(member).await.unwrap().channels;
|
||||||
|
assert_dag(
|
||||||
|
channels,
|
||||||
|
&[(active_channel, None), (vim_channel, Some(active_channel))],
|
||||||
|
);
|
||||||
|
|
||||||
|
db.set_channel_member_role(vim_channel, admin, guest, ChannelRole::Banned)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(db
|
||||||
|
.transaction(|tx| async move {
|
||||||
|
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
let mut members = db
|
||||||
|
.get_channel_participant_details(vim_channel, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
members.sort_by_key(|member| member.user_id);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
members,
|
||||||
|
&[
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: admin.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
|
role: proto::ChannelRole::Admin.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: member.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||||
|
role: proto::ChannelRole::Member.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: guest.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
|
role: proto::ChannelRole::Banned.into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
db.remove_channel_member(vim_channel, guest, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.invite_channel_member(zed_channel, guest, admin, ChannelRole::Guest)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// currently people invited to parent channels are not shown here
|
||||||
|
let mut members = db
|
||||||
|
.get_channel_participant_details(vim_channel, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
members.sort_by_key(|member| member.user_id);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
members,
|
||||||
|
&[
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: admin.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
|
role: proto::ChannelRole::Admin.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: member.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||||
|
role: proto::ChannelRole::Member.into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
db.respond_to_channel_invite(zed_channel, guest, true)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.transaction(|tx| async move {
|
||||||
|
db.check_user_is_channel_participant(zed_channel, guest, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(db
|
||||||
|
.transaction(|tx| async move {
|
||||||
|
db.check_user_is_channel_participant(active_channel, guest, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.is_err(),);
|
||||||
|
|
||||||
|
db.transaction(|tx| async move {
|
||||||
|
db.check_user_is_channel_participant(vim_channel, guest, &*tx)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut members = db
|
||||||
|
.get_channel_participant_details(vim_channel, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
members.sort_by_key(|member| member.user_id);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
members,
|
||||||
|
&[
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: admin.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::Member.into(),
|
||||||
|
role: proto::ChannelRole::Admin.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: member.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||||
|
role: proto::ChannelRole::Member.into(),
|
||||||
|
},
|
||||||
|
proto::ChannelMember {
|
||||||
|
user_id: guest.to_proto(),
|
||||||
|
kind: proto::channel_member::Kind::AncestorMember.into(),
|
||||||
|
role: proto::ChannelRole::Guest.into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
let channels = db.get_channels_for_user(guest).await.unwrap().channels;
|
||||||
|
assert_dag(
|
||||||
|
channels,
|
||||||
|
&[(zed_channel, None), (vim_channel, Some(zed_channel))],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
test_both_dbs!(
|
||||||
|
test_user_joins_correct_channel,
|
||||||
|
test_user_joins_correct_channel_postgres,
|
||||||
|
test_user_joins_correct_channel_sqlite
|
||||||
|
);
|
||||||
|
|
||||||
|
async fn test_user_joins_correct_channel(db: &Arc<Database>) {
|
||||||
|
let admin = new_test_user(db, "admin@example.com").await;
|
||||||
|
|
||||||
|
let zed_channel = db.create_root_channel("zed", admin).await.unwrap();
|
||||||
|
|
||||||
|
let active_channel = db
|
||||||
|
.create_channel("active", Some(zed_channel), admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vim_channel = db
|
||||||
|
.create_channel("vim", Some(active_channel), admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vim2_channel = db
|
||||||
|
.create_channel("vim2", Some(vim_channel), admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.set_channel_visibility(zed_channel, crate::db::ChannelVisibility::Public, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.set_channel_visibility(vim_channel, crate::db::ChannelVisibility::Public, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.set_channel_visibility(vim2_channel, crate::db::ChannelVisibility::Public, admin)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let most_public = db
|
||||||
|
.transaction(
|
||||||
|
|tx| async move { db.most_public_ancestor_for_channel(vim_channel, &*tx).await },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(most_public, Some(zed_channel))
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
|
fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)]) {
|
||||||
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
let mut actual_map: HashMap<ChannelId, HashSet<ChannelId>> = HashMap::default();
|
||||||
|
@ -859,3 +1105,19 @@ fn assert_dag(actual: ChannelGraph, expected: &[(ChannelId, Option<ChannelId>)])
|
||||||
|
|
||||||
pretty_assertions::assert_eq!(actual_map, expected_map)
|
pretty_assertions::assert_eq!(actual_map, expected_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GITHUB_USER_ID: AtomicI32 = AtomicI32::new(5);
|
||||||
|
|
||||||
|
async fn new_test_user(db: &Arc<Database>, email: &str) -> UserId {
|
||||||
|
db.create_user(
|
||||||
|
email,
|
||||||
|
false,
|
||||||
|
NewUserParams {
|
||||||
|
github_login: email[0..email.find("@").unwrap()].to_string(),
|
||||||
|
github_user_id: GITHUB_USER_ID.fetch_add(1, Ordering::SeqCst),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.user_id
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{Database, MessageId, NewUserParams},
|
db::{ChannelRole, Database, MessageId, NewUserParams},
|
||||||
test_both_dbs,
|
test_both_dbs,
|
||||||
};
|
};
|
||||||
use channel::mentions_to_proto;
|
use channel::mentions_to_proto;
|
||||||
|
@ -158,12 +158,13 @@ async fn test_unseen_channel_messages(db: &Arc<Database>) {
|
||||||
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
|
let channel_1 = db.create_channel("channel", None, user).await.unwrap();
|
||||||
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
|
let channel_2 = db.create_channel("channel-2", None, user).await.unwrap();
|
||||||
|
|
||||||
db.invite_channel_member(channel_1, observer, user, false)
|
db.invite_channel_member(channel_1, observer, user, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.invite_channel_member(channel_2, observer, user, false)
|
db.invite_channel_member(channel_2, observer, user, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
db.respond_to_channel_invite(channel_1, observer, true)
|
db.respond_to_channel_invite(channel_1, observer, true)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -341,7 +342,7 @@ async fn test_channel_message_mentions(db: &Arc<Database>) {
|
||||||
.user_id;
|
.user_id;
|
||||||
|
|
||||||
let channel = db.create_channel("channel", None, user_a).await.unwrap();
|
let channel = db.create_channel("channel", None, user_a).await.unwrap();
|
||||||
db.invite_channel_member(channel, user_b, user_a, false)
|
db.invite_channel_member(channel, user_b, user_a, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.respond_to_channel_invite(channel, user_b, true)
|
db.respond_to_channel_invite(channel, user_b, true)
|
||||||
|
|
|
@ -3,8 +3,8 @@ mod connection_pool;
|
||||||
use crate::{
|
use crate::{
|
||||||
auth,
|
auth,
|
||||||
db::{
|
db::{
|
||||||
self, BufferId, ChannelId, ChannelsForUser, Database, MessageId, ProjectId, RoomId,
|
self, BufferId, ChannelId, ChannelVisibility, ChannelsForUser, Database, MessageId,
|
||||||
ServerId, User, UserId,
|
ProjectId, RoomId, ServerId, User, UserId,
|
||||||
},
|
},
|
||||||
executor::Executor,
|
executor::Executor,
|
||||||
AppState, Result,
|
AppState, Result,
|
||||||
|
@ -256,7 +256,8 @@ impl Server {
|
||||||
.add_request_handler(delete_channel)
|
.add_request_handler(delete_channel)
|
||||||
.add_request_handler(invite_channel_member)
|
.add_request_handler(invite_channel_member)
|
||||||
.add_request_handler(remove_channel_member)
|
.add_request_handler(remove_channel_member)
|
||||||
.add_request_handler(set_channel_member_admin)
|
.add_request_handler(set_channel_member_role)
|
||||||
|
.add_request_handler(set_channel_visibility)
|
||||||
.add_request_handler(rename_channel)
|
.add_request_handler(rename_channel)
|
||||||
.add_request_handler(join_channel_buffer)
|
.add_request_handler(join_channel_buffer)
|
||||||
.add_request_handler(leave_channel_buffer)
|
.add_request_handler(leave_channel_buffer)
|
||||||
|
@ -979,6 +980,13 @@ async fn join_room(
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let room_id = RoomId::from_proto(request.id);
|
let room_id = RoomId::from_proto(request.id);
|
||||||
|
|
||||||
|
let channel_id = session.db().await.channel_id_for_room(room_id).await?;
|
||||||
|
|
||||||
|
if let Some(channel_id) = channel_id {
|
||||||
|
return join_channel_internal(channel_id, Box::new(response), session).await;
|
||||||
|
}
|
||||||
|
|
||||||
let joined_room = {
|
let joined_room = {
|
||||||
let room = session
|
let room = session
|
||||||
.db()
|
.db()
|
||||||
|
@ -994,16 +1002,6 @@ async fn join_room(
|
||||||
room.into_inner()
|
room.into_inner()
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(channel_id) = joined_room.channel_id {
|
|
||||||
channel_updated(
|
|
||||||
channel_id,
|
|
||||||
&joined_room.room,
|
|
||||||
&joined_room.channel_members,
|
|
||||||
&session.peer,
|
|
||||||
&*session.connection_pool().await,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for connection_id in session
|
for connection_id in session
|
||||||
.connection_pool()
|
.connection_pool()
|
||||||
.await
|
.await
|
||||||
|
@ -1041,7 +1039,7 @@ async fn join_room(
|
||||||
|
|
||||||
response.send(proto::JoinRoomResponse {
|
response.send(proto::JoinRoomResponse {
|
||||||
room: Some(joined_room.room),
|
room: Some(joined_room.room),
|
||||||
channel_id: joined_room.channel_id.map(|id| id.to_proto()),
|
channel_id: None,
|
||||||
live_kit_connection_info,
|
live_kit_connection_info,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
@ -2224,6 +2222,7 @@ async fn create_channel(
|
||||||
let channel = proto::Channel {
|
let channel = proto::Channel {
|
||||||
id: id.to_proto(),
|
id: id.to_proto(),
|
||||||
name: request.name,
|
name: request.name,
|
||||||
|
visibility: proto::ChannelVisibility::Members as i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
response.send(proto::CreateChannelResponse {
|
response.send(proto::CreateChannelResponse {
|
||||||
|
@ -2297,17 +2296,20 @@ async fn invite_channel_member(
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
let invitee_id = UserId::from_proto(request.user_id);
|
let invitee_id = UserId::from_proto(request.user_id);
|
||||||
let notifications = db
|
let notifications = db
|
||||||
.invite_channel_member(channel_id, invitee_id, session.user_id, request.admin)
|
.invite_channel_member(
|
||||||
|
channel_id,
|
||||||
|
invitee_id,
|
||||||
|
session.user_id,
|
||||||
|
request.role().into(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (channel, _) = db
|
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||||
.get_channel(channel_id, session.user_id)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
|
||||||
|
|
||||||
let mut update = proto::UpdateChannels::default();
|
let mut update = proto::UpdateChannels::default();
|
||||||
update.channel_invitations.push(proto::Channel {
|
update.channel_invitations.push(proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
|
visibility: channel.visibility.into(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2361,27 +2363,63 @@ async fn remove_channel_member(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_channel_member_admin(
|
async fn set_channel_visibility(
|
||||||
request: proto::SetChannelMemberAdmin,
|
request: proto::SetChannelVisibility,
|
||||||
response: Response<proto::SetChannelMemberAdmin>,
|
response: Response<proto::SetChannelVisibility>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db = session.db().await;
|
||||||
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
|
let visibility = request.visibility().into();
|
||||||
|
|
||||||
|
let channel = db
|
||||||
|
.set_channel_visibility(channel_id, visibility, session.user_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut update = proto::UpdateChannels::default();
|
||||||
|
update.channels.push(proto::Channel {
|
||||||
|
id: channel.id.to_proto(),
|
||||||
|
name: channel.name,
|
||||||
|
visibility: channel.visibility.into(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let member_ids = db.get_channel_members(channel_id).await?;
|
||||||
|
|
||||||
|
let connection_pool = session.connection_pool().await;
|
||||||
|
for member_id in member_ids {
|
||||||
|
for connection_id in connection_pool.user_connection_ids(member_id) {
|
||||||
|
session.peer.send(connection_id, update.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.send(proto::Ack {})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_channel_member_role(
|
||||||
|
request: proto::SetChannelMemberRole,
|
||||||
|
response: Response<proto::SetChannelMemberRole>,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
let member_id = UserId::from_proto(request.user_id);
|
let member_id = UserId::from_proto(request.user_id);
|
||||||
db.set_channel_member_admin(channel_id, session.user_id, member_id, request.admin)
|
let channel_member = db
|
||||||
|
.set_channel_member_role(
|
||||||
|
channel_id,
|
||||||
|
session.user_id,
|
||||||
|
member_id,
|
||||||
|
request.role().into(),
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (channel, has_accepted) = db
|
let channel = db.get_channel(channel_id, session.user_id).await?;
|
||||||
.get_channel(channel_id, member_id)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| anyhow!("channel not found"))?;
|
|
||||||
|
|
||||||
let mut update = proto::UpdateChannels::default();
|
let mut update = proto::UpdateChannels::default();
|
||||||
if has_accepted {
|
if channel_member.accepted {
|
||||||
update.channel_permissions.push(proto::ChannelPermission {
|
update.channel_permissions.push(proto::ChannelPermission {
|
||||||
channel_id: channel.id.to_proto(),
|
channel_id: channel.id.to_proto(),
|
||||||
is_admin: request.admin,
|
role: request.role,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2404,13 +2442,14 @@ async fn rename_channel(
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
let new_name = db
|
let channel = db
|
||||||
.rename_channel(channel_id, session.user_id, &request.name)
|
.rename_channel(channel_id, session.user_id, &request.name)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let channel = proto::Channel {
|
let channel = proto::Channel {
|
||||||
id: request.channel_id,
|
id: channel.id.to_proto(),
|
||||||
name: new_name,
|
name: channel.name,
|
||||||
|
visibility: channel.visibility.into(),
|
||||||
};
|
};
|
||||||
response.send(proto::RenameChannelResponse {
|
response.send(proto::RenameChannelResponse {
|
||||||
channel: Some(channel.clone()),
|
channel: Some(channel.clone()),
|
||||||
|
@ -2448,6 +2487,7 @@ async fn link_channel(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|channel| proto::Channel {
|
.map(|channel| proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
|
visibility: channel.visibility.into(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -2539,6 +2579,7 @@ async fn move_channel(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|channel| proto::Channel {
|
.map(|channel| proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
|
visibility: channel.visibility.into(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -2564,7 +2605,7 @@ async fn get_channel_members(
|
||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
let members = db
|
let members = db
|
||||||
.get_channel_member_details(channel_id, session.user_id)
|
.get_channel_participant_details(channel_id, session.user_id)
|
||||||
.await?;
|
.await?;
|
||||||
response.send(proto::GetChannelMembersResponse { members })?;
|
response.send(proto::GetChannelMembersResponse { members })?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -2581,51 +2622,16 @@ async fn respond_to_channel_invite(
|
||||||
.respond_to_channel_invite(channel_id, session.user_id, request.accept)
|
.respond_to_channel_invite(channel_id, session.user_id, request.accept)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let mut update = proto::UpdateChannels::default();
|
|
||||||
update
|
|
||||||
.remove_channel_invitations
|
|
||||||
.push(channel_id.to_proto());
|
|
||||||
if request.accept {
|
if request.accept {
|
||||||
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
channel_membership_updated(db, channel_id, &session).await?;
|
||||||
|
} else {
|
||||||
|
let mut update = proto::UpdateChannels::default();
|
||||||
update
|
update
|
||||||
.channels
|
.remove_channel_invitations
|
||||||
.extend(
|
.push(channel_id.to_proto());
|
||||||
result
|
session.peer.send(session.connection_id, update)?;
|
||||||
.channels
|
|
||||||
.channels
|
|
||||||
.into_iter()
|
|
||||||
.map(|channel| proto::Channel {
|
|
||||||
id: channel.id.to_proto(),
|
|
||||||
name: channel.name,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
update.unseen_channel_messages = result.channel_messages;
|
|
||||||
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
|
||||||
update.insert_edge = result.channels.edges;
|
|
||||||
update
|
|
||||||
.channel_participants
|
|
||||||
.extend(
|
|
||||||
result
|
|
||||||
.channel_participants
|
|
||||||
.into_iter()
|
|
||||||
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
|
||||||
channel_id: channel_id.to_proto(),
|
|
||||||
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
update
|
|
||||||
.channel_permissions
|
|
||||||
.extend(
|
|
||||||
result
|
|
||||||
.channels_with_admin_privileges
|
|
||||||
.into_iter()
|
|
||||||
.map(|channel_id| proto::ChannelPermission {
|
|
||||||
channel_id: channel_id.to_proto(),
|
|
||||||
is_admin: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
session.peer.send(session.connection_id, update)?;
|
|
||||||
send_notifications(
|
send_notifications(
|
||||||
&*session.connection_pool().await,
|
&*session.connection_pool().await,
|
||||||
&session.peer,
|
&session.peer,
|
||||||
|
@ -2636,25 +2642,92 @@ async fn respond_to_channel_invite(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn channel_membership_updated(
|
||||||
|
db: tokio::sync::MutexGuard<'_, DbHandle>,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
session: &Session,
|
||||||
|
) -> Result<(), crate::Error> {
|
||||||
|
let mut update = proto::UpdateChannels::default();
|
||||||
|
update
|
||||||
|
.remove_channel_invitations
|
||||||
|
.push(channel_id.to_proto());
|
||||||
|
|
||||||
|
let result = db.get_channel_for_user(channel_id, session.user_id).await?;
|
||||||
|
update.channels.extend(
|
||||||
|
result
|
||||||
|
.channels
|
||||||
|
.channels
|
||||||
|
.into_iter()
|
||||||
|
.map(|channel| proto::Channel {
|
||||||
|
id: channel.id.to_proto(),
|
||||||
|
visibility: channel.visibility.into(),
|
||||||
|
name: channel.name,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
update.unseen_channel_messages = result.channel_messages;
|
||||||
|
update.unseen_channel_buffer_changes = result.unseen_buffer_changes;
|
||||||
|
update.insert_edge = result.channels.edges;
|
||||||
|
update
|
||||||
|
.channel_participants
|
||||||
|
.extend(
|
||||||
|
result
|
||||||
|
.channel_participants
|
||||||
|
.into_iter()
|
||||||
|
.map(|(channel_id, user_ids)| proto::ChannelParticipants {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
participant_user_ids: user_ids.into_iter().map(UserId::to_proto).collect(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
update
|
||||||
|
.channel_permissions
|
||||||
|
.extend(
|
||||||
|
result
|
||||||
|
.channels_with_admin_privileges
|
||||||
|
.into_iter()
|
||||||
|
.map(|channel_id| proto::ChannelPermission {
|
||||||
|
channel_id: channel_id.to_proto(),
|
||||||
|
role: proto::ChannelRole::Admin.into(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
session.peer.send(session.connection_id, update)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn join_channel(
|
async fn join_channel(
|
||||||
request: proto::JoinChannel,
|
request: proto::JoinChannel,
|
||||||
response: Response<proto::JoinChannel>,
|
response: Response<proto::JoinChannel>,
|
||||||
session: Session,
|
session: Session,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let channel_id = ChannelId::from_proto(request.channel_id);
|
let channel_id = ChannelId::from_proto(request.channel_id);
|
||||||
let live_kit_room = format!("channel-{}", nanoid::nanoid!(30));
|
join_channel_internal(channel_id, Box::new(response), session).await
|
||||||
|
}
|
||||||
|
|
||||||
|
trait JoinChannelInternalResponse {
|
||||||
|
fn send(self, result: proto::JoinRoomResponse) -> Result<()>;
|
||||||
|
}
|
||||||
|
impl JoinChannelInternalResponse for Response<proto::JoinChannel> {
|
||||||
|
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||||
|
Response::<proto::JoinChannel>::send(self, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl JoinChannelInternalResponse for Response<proto::JoinRoom> {
|
||||||
|
fn send(self, result: proto::JoinRoomResponse) -> Result<()> {
|
||||||
|
Response::<proto::JoinRoom>::send(self, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn join_channel_internal(
|
||||||
|
channel_id: ChannelId,
|
||||||
|
response: Box<impl JoinChannelInternalResponse>,
|
||||||
|
session: Session,
|
||||||
|
) -> Result<()> {
|
||||||
let joined_room = {
|
let joined_room = {
|
||||||
leave_room_for_session(&session).await?;
|
leave_room_for_session(&session).await?;
|
||||||
let db = session.db().await;
|
let db = session.db().await;
|
||||||
|
|
||||||
let room_id = db
|
let (joined_room, joined_channel) = db
|
||||||
.get_or_create_channel_room(channel_id, &live_kit_room, &*RELEASE_CHANNEL_NAME)
|
.join_channel(
|
||||||
.await?;
|
channel_id,
|
||||||
|
|
||||||
let joined_room = db
|
|
||||||
.join_room(
|
|
||||||
room_id,
|
|
||||||
session.user_id,
|
session.user_id,
|
||||||
session.connection_id,
|
session.connection_id,
|
||||||
RELEASE_CHANNEL_NAME.as_str(),
|
RELEASE_CHANNEL_NAME.as_str(),
|
||||||
|
@ -2681,9 +2754,13 @@ async fn join_channel(
|
||||||
live_kit_connection_info,
|
live_kit_connection_info,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
if let Some(joined_channel) = joined_channel {
|
||||||
|
channel_membership_updated(db, joined_channel, &session).await?
|
||||||
|
}
|
||||||
|
|
||||||
room_updated(&joined_room.room, &session.peer);
|
room_updated(&joined_room.room, &session.peer);
|
||||||
|
|
||||||
joined_room.into_inner()
|
joined_room
|
||||||
};
|
};
|
||||||
|
|
||||||
channel_updated(
|
channel_updated(
|
||||||
|
@ -2695,7 +2772,6 @@ async fn join_channel(
|
||||||
);
|
);
|
||||||
|
|
||||||
update_user_contacts(session.user_id, &session).await?;
|
update_user_contacts(session.user_id, &session).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3152,6 +3228,7 @@ fn build_initial_channels_update(
|
||||||
update.channels.push(proto::Channel {
|
update.channels.push(proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
|
visibility: channel.visibility.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3176,7 +3253,7 @@ fn build_initial_channels_update(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|id| proto::ChannelPermission {
|
.map(|id| proto::ChannelPermission {
|
||||||
channel_id: id.to_proto(),
|
channel_id: id.to_proto(),
|
||||||
is_admin: true,
|
role: proto::ChannelRole::Admin.into(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -3184,6 +3261,8 @@ fn build_initial_channels_update(
|
||||||
update.channel_invitations.push(proto::Channel {
|
update.channel_invitations.push(proto::Channel {
|
||||||
id: channel.id.to_proto(),
|
id: channel.id.to_proto(),
|
||||||
name: channel.name,
|
name: channel.name,
|
||||||
|
// TODO: Visibility
|
||||||
|
visibility: ChannelVisibility::Public.into(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,10 @@ use collections::HashMap;
|
||||||
use editor::{Anchor, Editor, ToOffset};
|
use editor::{Anchor, Editor, ToOffset};
|
||||||
use futures::future;
|
use futures::future;
|
||||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||||
use rpc::{proto::PeerId, RECEIVE_TIMEOUT};
|
use rpc::{
|
||||||
|
proto::{self, PeerId},
|
||||||
|
RECEIVE_TIMEOUT,
|
||||||
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
@ -445,6 +448,7 @@ fn channel(id: u64, name: &'static str) -> Channel {
|
||||||
Channel {
|
Channel {
|
||||||
id,
|
id,
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
|
visibility: proto::ChannelVisibility::Members,
|
||||||
unseen_note_version: None,
|
unseen_note_version: None,
|
||||||
unseen_message_id: None,
|
unseen_message_id: None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,10 @@ use call::ActiveCall;
|
||||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||||
use client::User;
|
use client::User;
|
||||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
use gpui::{executor::Deterministic, ModelHandle, TestAppContext};
|
||||||
use rpc::{proto, RECEIVE_TIMEOUT};
|
use rpc::{
|
||||||
|
proto::{self, ChannelRole},
|
||||||
|
RECEIVE_TIMEOUT,
|
||||||
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -68,7 +71,12 @@ async fn test_core_channels(
|
||||||
.update(cx_a, |store, cx| {
|
.update(cx_a, |store, cx| {
|
||||||
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
assert!(!store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||||
|
|
||||||
let invite = store.invite_member(channel_a_id, client_b.user_id().unwrap(), false, cx);
|
let invite = store.invite_member(
|
||||||
|
channel_a_id,
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Member,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
// Make sure we're synchronously storing the pending invite
|
// Make sure we're synchronously storing the pending invite
|
||||||
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
assert!(store.has_pending_channel_invite(channel_a_id, client_b.user_id().unwrap()));
|
||||||
|
@ -103,12 +111,12 @@ async fn test_core_channels(
|
||||||
&[
|
&[
|
||||||
(
|
(
|
||||||
client_a.user_id().unwrap(),
|
client_a.user_id().unwrap(),
|
||||||
true,
|
proto::ChannelRole::Admin,
|
||||||
proto::channel_member::Kind::Member,
|
proto::channel_member::Kind::Member,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
client_b.user_id().unwrap(),
|
client_b.user_id().unwrap(),
|
||||||
false,
|
proto::ChannelRole::Member,
|
||||||
proto::channel_member::Kind::Invitee,
|
proto::channel_member::Kind::Invitee,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -183,7 +191,12 @@ async fn test_core_channels(
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |store, cx| {
|
.update(cx_a, |store, cx| {
|
||||||
store.set_member_admin(channel_a_id, client_b.user_id().unwrap(), true, cx)
|
store.set_member_role(
|
||||||
|
channel_a_id,
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Admin,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -305,12 +318,12 @@ fn assert_participants_eq(participants: &[Arc<User>], expected_partitipants: &[u
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn assert_members_eq(
|
fn assert_members_eq(
|
||||||
members: &[ChannelMembership],
|
members: &[ChannelMembership],
|
||||||
expected_members: &[(u64, bool, proto::channel_member::Kind)],
|
expected_members: &[(u64, proto::ChannelRole, proto::channel_member::Kind)],
|
||||||
) {
|
) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
members
|
members
|
||||||
.iter()
|
.iter()
|
||||||
.map(|member| (member.user.id, member.admin, member.kind))
|
.map(|member| (member.user.id, member.role, member.kind))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
expected_members
|
expected_members
|
||||||
);
|
);
|
||||||
|
@ -611,7 +624,12 @@ async fn test_permissions_update_while_invited(
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, cx| {
|
.update(cx_a, |channel_store, cx| {
|
||||||
channel_store.invite_member(rust_id, client_b.user_id().unwrap(), false, cx)
|
channel_store.invite_member(
|
||||||
|
rust_id,
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Member,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -634,7 +652,12 @@ async fn test_permissions_update_while_invited(
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, cx| {
|
.update(cx_a, |channel_store, cx| {
|
||||||
channel_store.set_member_admin(rust_id, client_b.user_id().unwrap(), true, cx)
|
channel_store.set_member_role(
|
||||||
|
rust_id,
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Admin,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -803,7 +826,12 @@ async fn test_lost_channel_creation(
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |channel_store, cx| {
|
.update(cx_a, |channel_store, cx| {
|
||||||
channel_store.invite_member(channel_id, client_b.user_id().unwrap(), false, cx)
|
channel_store.invite_member(
|
||||||
|
channel_id,
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
proto::ChannelRole::Member,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -884,6 +912,119 @@ async fn test_lost_channel_creation(
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_guest_access(
|
||||||
|
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 channels = server
|
||||||
|
.make_channel_tree(&[("channel-a", None)], (&client_a, cx_a))
|
||||||
|
.await;
|
||||||
|
let channel_a_id = channels[0];
|
||||||
|
|
||||||
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
|
||||||
|
// should not be allowed to join
|
||||||
|
assert!(active_call_b
|
||||||
|
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel_store, cx| {
|
||||||
|
channel_store.set_channel_visibility(channel_a_id, proto::ChannelVisibility::Public, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
active_call_b
|
||||||
|
.update(cx_b, |call, cx| call.join_channel(channel_a_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
assert!(client_b
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_b, |channel_store, _| channel_store
|
||||||
|
.channel_for_id(channel_a_id)
|
||||||
|
.is_some()));
|
||||||
|
|
||||||
|
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||||
|
let participants = channel_store.channel_participants(channel_a_id);
|
||||||
|
assert_eq!(participants.len(), 1);
|
||||||
|
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_invite_access(
|
||||||
|
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 channels = server
|
||||||
|
.make_channel_tree(
|
||||||
|
&[("channel-a", None), ("channel-b", Some("channel-a"))],
|
||||||
|
(&client_a, cx_a),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let channel_a_id = channels[0];
|
||||||
|
let channel_b_id = channels[0];
|
||||||
|
|
||||||
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
|
||||||
|
// should not be allowed to join
|
||||||
|
assert!(active_call_b
|
||||||
|
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||||
|
.await
|
||||||
|
.is_err());
|
||||||
|
|
||||||
|
client_a
|
||||||
|
.channel_store()
|
||||||
|
.update(cx_a, |channel_store, cx| {
|
||||||
|
channel_store.invite_member(
|
||||||
|
channel_a_id,
|
||||||
|
client_b.user_id().unwrap(),
|
||||||
|
ChannelRole::Member,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
active_call_b
|
||||||
|
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
|
||||||
|
client_b.channel_store().update(cx_b, |channel_store, _| {
|
||||||
|
assert!(channel_store.channel_for_id(channel_b_id).is_some());
|
||||||
|
assert!(channel_store.channel_for_id(channel_a_id).is_some());
|
||||||
|
});
|
||||||
|
|
||||||
|
client_a.channel_store().update(cx_a, |channel_store, _| {
|
||||||
|
let participants = channel_store.channel_participants(channel_b_id);
|
||||||
|
assert_eq!(participants.len(), 1);
|
||||||
|
assert_eq!(participants[0].id, client_b.user_id().unwrap());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_channel_moving(
|
async fn test_channel_moving(
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::tests::TestServer;
|
||||||
use gpui::{executor::Deterministic, TestAppContext};
|
use gpui::{executor::Deterministic, TestAppContext};
|
||||||
use notifications::NotificationEvent;
|
use notifications::NotificationEvent;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::Notification;
|
use rpc::{proto, Notification};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
@ -120,7 +120,7 @@ async fn test_notifications(
|
||||||
client_a
|
client_a
|
||||||
.channel_store()
|
.channel_store()
|
||||||
.update(cx_a, |store, cx| {
|
.update(cx_a, |store, cx| {
|
||||||
store.invite_member(channel_id, client_b.id(), false, cx)
|
store.invite_member(channel_id, client_b.id(), proto::ChannelRole::Member, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use crate::db::ChannelRole;
|
||||||
|
|
||||||
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
use super::{run_randomized_test, RandomizedTest, TestClient, TestError, TestServer, UserTestPlan};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -50,7 +52,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
for user in &users[1..] {
|
for user in &users[1..] {
|
||||||
db.invite_channel_member(id, user.user_id, users[0].user_id, false)
|
db.invite_channel_member(id, user.user_id, users[0].user_id, ChannelRole::Member)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
db.respond_to_channel_invite(id, user.user_id, true)
|
db.respond_to_channel_invite(id, user.user_id, true)
|
||||||
|
|
|
@ -19,7 +19,7 @@ use node_runtime::FakeNodeRuntime;
|
||||||
use notifications::NotificationStore;
|
use notifications::NotificationStore;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{Project, WorktreeId};
|
use project::{Project, WorktreeId};
|
||||||
use rpc::RECEIVE_TIMEOUT;
|
use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT};
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Ref, RefCell, RefMut},
|
cell::{Ref, RefCell, RefMut},
|
||||||
|
@ -330,7 +330,7 @@ impl TestServer {
|
||||||
channel_store.invite_member(
|
channel_store.invite_member(
|
||||||
channel_id,
|
channel_id,
|
||||||
member_client.user_id().unwrap(),
|
member_client.user_id().unwrap(),
|
||||||
false,
|
ChannelRole::Member,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -623,7 +623,12 @@ impl TestClient {
|
||||||
cx_self
|
cx_self
|
||||||
.read(ChannelStore::global)
|
.read(ChannelStore::global)
|
||||||
.update(cx_self, |channel_store, cx| {
|
.update(cx_self, |channel_store, cx| {
|
||||||
channel_store.invite_member(channel, other_client.user_id().unwrap(), true, cx)
|
channel_store.invite_member(
|
||||||
|
channel,
|
||||||
|
other_client.user_id().unwrap(),
|
||||||
|
ChannelRole::Admin,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -345,8 +345,12 @@ impl ChatPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
fn render_message(&mut self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||||
let (message, is_continuation, is_last) = {
|
let (message, is_continuation, is_last, is_admin) = {
|
||||||
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
|
let active_chat = self.active_chat.as_ref().unwrap().0.read(cx);
|
||||||
|
let is_admin = self
|
||||||
|
.channel_store
|
||||||
|
.read(cx)
|
||||||
|
.is_user_admin(active_chat.channel().id);
|
||||||
let last_message = active_chat.message(ix.saturating_sub(1));
|
let last_message = active_chat.message(ix.saturating_sub(1));
|
||||||
let this_message = active_chat.message(ix);
|
let this_message = active_chat.message(ix);
|
||||||
let is_continuation = last_message.id != this_message.id
|
let is_continuation = last_message.id != this_message.id
|
||||||
|
@ -356,6 +360,7 @@ impl ChatPanel {
|
||||||
active_chat.message(ix).clone(),
|
active_chat.message(ix).clone(),
|
||||||
is_continuation,
|
is_continuation,
|
||||||
active_chat.message_count() == ix + 1,
|
active_chat.message_count() == ix + 1,
|
||||||
|
is_admin,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -376,12 +381,13 @@ impl ChatPanel {
|
||||||
};
|
};
|
||||||
|
|
||||||
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
|
let belongs_to_user = Some(message.sender.id) == self.client.user_id();
|
||||||
let message_id_to_remove =
|
let message_id_to_remove = if let (ChannelMessageId::Saved(id), true) =
|
||||||
if let (ChannelMessageId::Saved(id), true) = (message.id, belongs_to_user) {
|
(message.id, belongs_to_user || is_admin)
|
||||||
Some(id)
|
{
|
||||||
} else {
|
Some(id)
|
||||||
None
|
} else {
|
||||||
};
|
None
|
||||||
|
};
|
||||||
|
|
||||||
enum MessageBackgroundHighlight {}
|
enum MessageBackgroundHighlight {}
|
||||||
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
MouseEventHandler::new::<MessageBackgroundHighlight, _>(ix, cx, |state, cx| {
|
||||||
|
|
|
@ -232,7 +232,7 @@ mod tests {
|
||||||
avatar: None,
|
avatar: None,
|
||||||
}),
|
}),
|
||||||
kind: proto::channel_member::Kind::Member,
|
kind: proto::channel_member::Kind::Member,
|
||||||
admin: false,
|
role: proto::ChannelRole::Member,
|
||||||
},
|
},
|
||||||
ChannelMembership {
|
ChannelMembership {
|
||||||
user: Arc::new(User {
|
user: Arc::new(User {
|
||||||
|
@ -241,7 +241,7 @@ mod tests {
|
||||||
avatar: None,
|
avatar: None,
|
||||||
}),
|
}),
|
||||||
kind: proto::channel_member::Kind::Member,
|
kind: proto::channel_member::Kind::Member,
|
||||||
admin: false,
|
role: proto::ChannelRole::Member,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -11,7 +11,10 @@ use anyhow::Result;
|
||||||
use call::ActiveCall;
|
use call::ActiveCall;
|
||||||
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
|
use channel::{Channel, ChannelData, ChannelEvent, ChannelId, ChannelPath, ChannelStore};
|
||||||
use channel_modal::ChannelModal;
|
use channel_modal::ChannelModal;
|
||||||
use client::{proto::PeerId, Client, Contact, User, UserStore};
|
use client::{
|
||||||
|
proto::{self, PeerId},
|
||||||
|
Client, Contact, User, UserStore,
|
||||||
|
};
|
||||||
use contact_finder::ContactFinder;
|
use contact_finder::ContactFinder;
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
@ -428,7 +431,7 @@ enum ListEntry {
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
},
|
},
|
||||||
ParticipantScreen {
|
ParticipantScreen {
|
||||||
peer_id: PeerId,
|
peer_id: Option<PeerId>,
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
},
|
},
|
||||||
IncomingRequest(Arc<User>),
|
IncomingRequest(Arc<User>),
|
||||||
|
@ -442,6 +445,9 @@ enum ListEntry {
|
||||||
ChannelNotes {
|
ChannelNotes {
|
||||||
channel_id: ChannelId,
|
channel_id: ChannelId,
|
||||||
},
|
},
|
||||||
|
ChannelChat {
|
||||||
|
channel_id: ChannelId,
|
||||||
|
},
|
||||||
ChannelEditor {
|
ChannelEditor {
|
||||||
depth: usize,
|
depth: usize,
|
||||||
},
|
},
|
||||||
|
@ -602,6 +608,13 @@ impl CollabPanel {
|
||||||
ix,
|
ix,
|
||||||
cx,
|
cx,
|
||||||
),
|
),
|
||||||
|
ListEntry::ChannelChat { channel_id } => this.render_channel_chat(
|
||||||
|
*channel_id,
|
||||||
|
&theme.collab_panel,
|
||||||
|
is_selected,
|
||||||
|
ix,
|
||||||
|
cx,
|
||||||
|
),
|
||||||
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
ListEntry::ChannelInvite(channel) => Self::render_channel_invite(
|
||||||
channel.clone(),
|
channel.clone(),
|
||||||
this.channel_store.clone(),
|
this.channel_store.clone(),
|
||||||
|
@ -804,7 +817,8 @@ impl CollabPanel {
|
||||||
let room = room.read(cx);
|
let room = room.read(cx);
|
||||||
|
|
||||||
if let Some(channel_id) = room.channel_id() {
|
if let Some(channel_id) = room.channel_id() {
|
||||||
self.entries.push(ListEntry::ChannelNotes { channel_id })
|
self.entries.push(ListEntry::ChannelNotes { channel_id });
|
||||||
|
self.entries.push(ListEntry::ChannelChat { channel_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Populate the active user.
|
// Populate the active user.
|
||||||
|
@ -836,7 +850,13 @@ impl CollabPanel {
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
worktree_root_names: project.worktree_root_names.clone(),
|
worktree_root_names: project.worktree_root_names.clone(),
|
||||||
host_user_id: user_id,
|
host_user_id: user_id,
|
||||||
is_last: projects.peek().is_none(),
|
is_last: projects.peek().is_none() && !room.is_screen_sharing(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if room.is_screen_sharing() {
|
||||||
|
self.entries.push(ListEntry::ParticipantScreen {
|
||||||
|
peer_id: None,
|
||||||
|
is_last: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -880,7 +900,7 @@ impl CollabPanel {
|
||||||
}
|
}
|
||||||
if !participant.video_tracks.is_empty() {
|
if !participant.video_tracks.is_empty() {
|
||||||
self.entries.push(ListEntry::ParticipantScreen {
|
self.entries.push(ListEntry::ParticipantScreen {
|
||||||
peer_id: participant.peer_id,
|
peer_id: Some(participant.peer_id),
|
||||||
is_last: true,
|
is_last: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1225,14 +1245,18 @@ impl CollabPanel {
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
enum CallParticipant {}
|
enum CallParticipant {}
|
||||||
enum CallParticipantTooltip {}
|
enum CallParticipantTooltip {}
|
||||||
|
enum LeaveCallButton {}
|
||||||
|
enum LeaveCallTooltip {}
|
||||||
|
|
||||||
let collab_theme = &theme.collab_panel;
|
let collab_theme = &theme.collab_panel;
|
||||||
|
|
||||||
let is_current_user =
|
let is_current_user =
|
||||||
user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
user_store.read(cx).current_user().map(|user| user.id) == Some(user.id);
|
||||||
|
|
||||||
let content =
|
let content = MouseEventHandler::new::<CallParticipant, _>(
|
||||||
MouseEventHandler::new::<CallParticipant, _>(user.id as usize, cx, |mouse_state, _| {
|
user.id as usize,
|
||||||
|
cx,
|
||||||
|
|mouse_state, cx| {
|
||||||
let style = if is_current_user {
|
let style = if is_current_user {
|
||||||
*collab_theme
|
*collab_theme
|
||||||
.contact_row
|
.contact_row
|
||||||
|
@ -1268,14 +1292,32 @@ impl CollabPanel {
|
||||||
Label::new("Calling", collab_theme.calling_indicator.text.clone())
|
Label::new("Calling", collab_theme.calling_indicator.text.clone())
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(collab_theme.calling_indicator.container)
|
.with_style(collab_theme.calling_indicator.container)
|
||||||
.aligned(),
|
.aligned()
|
||||||
|
.into_any(),
|
||||||
)
|
)
|
||||||
} else if is_current_user {
|
} else if is_current_user {
|
||||||
Some(
|
Some(
|
||||||
Label::new("You", collab_theme.calling_indicator.text.clone())
|
MouseEventHandler::new::<LeaveCallButton, _>(0, cx, |state, _| {
|
||||||
.contained()
|
render_icon_button(
|
||||||
.with_style(collab_theme.calling_indicator.container)
|
theme
|
||||||
.aligned(),
|
.collab_panel
|
||||||
|
.leave_call_button
|
||||||
|
.style_for(is_selected, state),
|
||||||
|
"icons/exit.svg",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.on_click(MouseButton::Left, |_, _, cx| {
|
||||||
|
Self::leave_call(cx);
|
||||||
|
})
|
||||||
|
.with_tooltip::<LeaveCallTooltip>(
|
||||||
|
0,
|
||||||
|
"Leave call",
|
||||||
|
None,
|
||||||
|
theme.tooltip.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.into_any(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -1284,7 +1326,8 @@ impl CollabPanel {
|
||||||
.with_height(collab_theme.row_height)
|
.with_height(collab_theme.row_height)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style)
|
.with_style(style)
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
if is_current_user || is_pending || peer_id.is_none() {
|
if is_current_user || is_pending || peer_id.is_none() {
|
||||||
return content.into_any();
|
return content.into_any();
|
||||||
|
@ -1406,7 +1449,7 @@ impl CollabPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_participant_screen(
|
fn render_participant_screen(
|
||||||
peer_id: PeerId,
|
peer_id: Option<PeerId>,
|
||||||
is_last: bool,
|
is_last: bool,
|
||||||
is_selected: bool,
|
is_selected: bool,
|
||||||
theme: &theme::CollabPanel,
|
theme: &theme::CollabPanel,
|
||||||
|
@ -1421,8 +1464,8 @@ impl CollabPanel {
|
||||||
.unwrap_or(0.);
|
.unwrap_or(0.);
|
||||||
let tree_branch = theme.tree_branch;
|
let tree_branch = theme.tree_branch;
|
||||||
|
|
||||||
MouseEventHandler::new::<OpenSharedScreen, _>(
|
let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
|
||||||
peer_id.as_u64() as usize,
|
peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
|
||||||
cx,
|
cx,
|
||||||
|mouse_state, cx| {
|
|mouse_state, cx| {
|
||||||
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
|
||||||
|
@ -1460,16 +1503,20 @@ impl CollabPanel {
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(row.container)
|
.with_style(row.container)
|
||||||
},
|
},
|
||||||
)
|
);
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
if peer_id.is_none() {
|
||||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
return handler.into_any();
|
||||||
if let Some(workspace) = this.workspace.upgrade(cx) {
|
}
|
||||||
workspace.update(cx, |workspace, cx| {
|
handler
|
||||||
workspace.open_shared_screen(peer_id, cx)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
});
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
}
|
if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
})
|
workspace.update(cx, |workspace, cx| {
|
||||||
.into_any()
|
workspace.open_shared_screen(peer_id.unwrap(), cx)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||||
|
@ -1496,23 +1543,32 @@ impl CollabPanel {
|
||||||
enum AddChannel {}
|
enum AddChannel {}
|
||||||
|
|
||||||
let tooltip_style = &theme.tooltip;
|
let tooltip_style = &theme.tooltip;
|
||||||
|
let mut channel_link = None;
|
||||||
|
let mut channel_tooltip_text = None;
|
||||||
|
let mut channel_icon = None;
|
||||||
|
|
||||||
let text = match section {
|
let text = match section {
|
||||||
Section::ActiveCall => {
|
Section::ActiveCall => {
|
||||||
let channel_name = iife!({
|
let channel_name = iife!({
|
||||||
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
|
let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?;
|
||||||
|
|
||||||
let name = self
|
let channel = self.channel_store.read(cx).channel_for_id(channel_id)?;
|
||||||
.channel_store
|
|
||||||
.read(cx)
|
|
||||||
.channel_for_id(channel_id)?
|
|
||||||
.name
|
|
||||||
.as_str();
|
|
||||||
|
|
||||||
Some(name)
|
channel_link = Some(channel.link());
|
||||||
|
(channel_icon, channel_tooltip_text) = match channel.visibility {
|
||||||
|
proto::ChannelVisibility::Public => {
|
||||||
|
(Some("icons/public.svg"), Some("Copy public channel link."))
|
||||||
|
}
|
||||||
|
proto::ChannelVisibility::Members => {
|
||||||
|
(Some("icons/hash.svg"), Some("Copy private channel link."))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(channel.name.as_str())
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(name) = channel_name {
|
if let Some(name) = channel_name {
|
||||||
Cow::Owned(format!("#{}", name))
|
Cow::Owned(format!("{}", name))
|
||||||
} else {
|
} else {
|
||||||
Cow::Borrowed("Current Call")
|
Cow::Borrowed("Current Call")
|
||||||
}
|
}
|
||||||
|
@ -1527,28 +1583,30 @@ impl CollabPanel {
|
||||||
|
|
||||||
enum AddContact {}
|
enum AddContact {}
|
||||||
let button = match section {
|
let button = match section {
|
||||||
Section::ActiveCall => Some(
|
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||||
|
let channel_link_copy = channel_link.clone();
|
||||||
MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
|
MouseEventHandler::new::<AddContact, _>(0, cx, |state, _| {
|
||||||
render_icon_button(
|
render_icon_button(
|
||||||
theme
|
theme
|
||||||
.collab_panel
|
.collab_panel
|
||||||
.leave_call_button
|
.leave_call_button
|
||||||
.style_for(is_selected, state),
|
.style_for(is_selected, state),
|
||||||
"icons/exit.svg",
|
"icons/link.svg",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
.on_click(MouseButton::Left, |_, _, cx| {
|
.on_click(MouseButton::Left, move |_, _, cx| {
|
||||||
Self::leave_call(cx);
|
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||||
|
cx.write_to_clipboard(item)
|
||||||
})
|
})
|
||||||
.with_tooltip::<AddContact>(
|
.with_tooltip::<AddContact>(
|
||||||
0,
|
0,
|
||||||
"Leave call",
|
channel_tooltip_text.unwrap(),
|
||||||
None,
|
None,
|
||||||
tooltip_style.clone(),
|
tooltip_style.clone(),
|
||||||
cx,
|
cx,
|
||||||
),
|
)
|
||||||
),
|
}),
|
||||||
Section::Contacts => Some(
|
Section::Contacts => Some(
|
||||||
MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
|
MouseEventHandler::new::<LeaveCallContactList, _>(0, cx, |state, _| {
|
||||||
render_icon_button(
|
render_icon_button(
|
||||||
|
@ -1633,6 +1691,21 @@ impl CollabPanel {
|
||||||
theme.collab_panel.contact_username.container.margin.left,
|
theme.collab_panel.contact_username.container.margin.left,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
} else if let Some(channel_icon) = channel_icon {
|
||||||
|
Some(
|
||||||
|
Svg::new(channel_icon)
|
||||||
|
.with_color(header_style.text.color)
|
||||||
|
.constrained()
|
||||||
|
.with_max_width(icon_size)
|
||||||
|
.with_max_height(icon_size)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(icon_size)
|
||||||
|
.contained()
|
||||||
|
.with_margin_right(
|
||||||
|
theme.collab_panel.contact_username.container.margin.left,
|
||||||
|
),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
|
@ -1908,6 +1981,12 @@ impl CollabPanel {
|
||||||
let channel_id = channel.id;
|
let channel_id = channel.id;
|
||||||
let collab_theme = &theme.collab_panel;
|
let collab_theme = &theme.collab_panel;
|
||||||
let has_children = self.channel_store.read(cx).has_children(channel_id);
|
let has_children = self.channel_store.read(cx).has_children(channel_id);
|
||||||
|
let is_public = self
|
||||||
|
.channel_store
|
||||||
|
.read(cx)
|
||||||
|
.channel_for_id(channel_id)
|
||||||
|
.map(|channel| channel.visibility)
|
||||||
|
== Some(proto::ChannelVisibility::Public);
|
||||||
let other_selected =
|
let other_selected =
|
||||||
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
|
self.selected_channel().map(|channel| channel.0.id) == Some(channel.id);
|
||||||
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
|
let disclosed = has_children.then(|| !self.collapsed_channels.binary_search(&path).is_ok());
|
||||||
|
@ -1965,12 +2044,16 @@ impl CollabPanel {
|
||||||
|
|
||||||
Flex::<Self>::row()
|
Flex::<Self>::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Svg::new("icons/hash.svg")
|
Svg::new(if is_public {
|
||||||
.with_color(collab_theme.channel_hash.color)
|
"icons/public.svg"
|
||||||
.constrained()
|
} else {
|
||||||
.with_width(collab_theme.channel_hash.width)
|
"icons/hash.svg"
|
||||||
.aligned()
|
})
|
||||||
.left(),
|
.with_color(collab_theme.channel_hash.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(collab_theme.channel_hash.width)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
)
|
)
|
||||||
.with_child({
|
.with_child({
|
||||||
let style = collab_theme.channel_name.inactive_state();
|
let style = collab_theme.channel_name.inactive_state();
|
||||||
|
@ -2275,7 +2358,7 @@ impl CollabPanel {
|
||||||
.with_child(render_tree_branch(
|
.with_child(render_tree_branch(
|
||||||
tree_branch,
|
tree_branch,
|
||||||
&row.name.text,
|
&row.name.text,
|
||||||
true,
|
false,
|
||||||
vec2f(host_avatar_width, theme.row_height),
|
vec2f(host_avatar_width, theme.row_height),
|
||||||
cx.font_cache(),
|
cx.font_cache(),
|
||||||
))
|
))
|
||||||
|
@ -2308,6 +2391,62 @@ impl CollabPanel {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_channel_chat(
|
||||||
|
&self,
|
||||||
|
channel_id: ChannelId,
|
||||||
|
theme: &theme::CollabPanel,
|
||||||
|
is_selected: bool,
|
||||||
|
ix: usize,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> AnyElement<Self> {
|
||||||
|
enum ChannelChat {}
|
||||||
|
let host_avatar_width = theme
|
||||||
|
.contact_avatar
|
||||||
|
.width
|
||||||
|
.or(theme.contact_avatar.height)
|
||||||
|
.unwrap_or(0.);
|
||||||
|
|
||||||
|
MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
|
||||||
|
let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
|
||||||
|
let row = theme.project_row.in_state(is_selected).style_for(state);
|
||||||
|
|
||||||
|
Flex::<Self>::row()
|
||||||
|
.with_child(render_tree_branch(
|
||||||
|
tree_branch,
|
||||||
|
&row.name.text,
|
||||||
|
true,
|
||||||
|
vec2f(host_avatar_width, theme.row_height),
|
||||||
|
cx.font_cache(),
|
||||||
|
))
|
||||||
|
.with_child(
|
||||||
|
Svg::new("icons/conversations.svg")
|
||||||
|
.with_color(theme.channel_hash.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(theme.channel_hash.width)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
.with_child(
|
||||||
|
Label::new("chat", theme.channel_name.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.channel_name.container)
|
||||||
|
.aligned()
|
||||||
|
.left()
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.constrained()
|
||||||
|
.with_height(theme.row_height)
|
||||||
|
.contained()
|
||||||
|
.with_style(*theme.channel_row.style_for(is_selected, state))
|
||||||
|
.with_padding_left(theme.channel_row.default_style().padding.left)
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
fn render_channel_invite(
|
fn render_channel_invite(
|
||||||
channel: Arc<Channel>,
|
channel: Arc<Channel>,
|
||||||
channel_store: ModelHandle<ChannelStore>,
|
channel_store: ModelHandle<ChannelStore>,
|
||||||
|
@ -2771,6 +2910,9 @@ impl CollabPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ListEntry::ParticipantScreen { peer_id, .. } => {
|
ListEntry::ParticipantScreen { peer_id, .. } => {
|
||||||
|
let Some(peer_id) = peer_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||||
workspace.update(cx, |workspace, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
workspace.open_shared_screen(*peer_id, cx)
|
workspace.open_shared_screen(*peer_id, cx)
|
||||||
|
@ -3499,6 +3641,14 @@ impl PartialEq for ListEntry {
|
||||||
return channel_id == other_id;
|
return channel_id == other_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ListEntry::ChannelChat { channel_id } => {
|
||||||
|
if let ListEntry::ChannelChat {
|
||||||
|
channel_id: other_id,
|
||||||
|
} = other
|
||||||
|
{
|
||||||
|
return channel_id == other_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
ListEntry::ChannelInvite(channel_1) => {
|
ListEntry::ChannelInvite(channel_1) => {
|
||||||
if let ListEntry::ChannelInvite(channel_2) = other {
|
if let ListEntry::ChannelInvite(channel_2) = other {
|
||||||
return channel_1.id == channel_2.id;
|
return channel_1.id == channel_2.id;
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
use channel::{ChannelId, ChannelMembership, ChannelStore};
|
||||||
use client::{proto, User, UserId, UserStore};
|
use client::{
|
||||||
|
proto::{self, ChannelRole, ChannelVisibility},
|
||||||
|
User, UserId, UserStore,
|
||||||
|
};
|
||||||
use context_menu::{ContextMenu, ContextMenuItem};
|
use context_menu::{ContextMenu, ContextMenuItem};
|
||||||
use fuzzy::{match_strings, StringMatchCandidate};
|
use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::*,
|
elements::*,
|
||||||
platform::{CursorStyle, MouseButton},
|
platform::{CursorStyle, MouseButton},
|
||||||
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
|
||||||
|
ViewHandle,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -96,11 +100,14 @@ impl ChannelModal {
|
||||||
let channel_id = self.channel_id;
|
let channel_id = self.channel_id;
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
if mode == Mode::ManageMembers {
|
if mode == Mode::ManageMembers {
|
||||||
let members = channel_store
|
let mut members = channel_store
|
||||||
.update(&mut cx, |channel_store, cx| {
|
.update(&mut cx, |channel_store, cx| {
|
||||||
channel_store.get_channel_member_details(channel_id, cx)
|
channel_store.get_channel_member_details(channel_id, cx)
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.picker
|
this.picker
|
||||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||||
|
@ -182,6 +189,81 @@ impl View for ChannelModal {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_visibility(
|
||||||
|
channel_id: ChannelId,
|
||||||
|
visibility: ChannelVisibility,
|
||||||
|
theme: &theme::TabbedModal,
|
||||||
|
cx: &mut ViewContext<ChannelModal>,
|
||||||
|
) -> AnyElement<ChannelModal> {
|
||||||
|
enum TogglePublic {}
|
||||||
|
|
||||||
|
if visibility == ChannelVisibility::Members {
|
||||||
|
return Flex::row()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||||
|
let style = theme.visibility_toggle.style_for(state);
|
||||||
|
Label::new(format!("{}", "Public access: OFF"), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container.clone())
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.channel_store
|
||||||
|
.update(cx, |channel_store, cx| {
|
||||||
|
channel_store.set_channel_visibility(
|
||||||
|
channel_id,
|
||||||
|
ChannelVisibility::Public,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
)
|
||||||
|
.into_any();
|
||||||
|
}
|
||||||
|
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
|
||||||
|
let style = theme.visibility_toggle.style_for(state);
|
||||||
|
Label::new(format!("{}", "Public access: ON"), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container.clone())
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
this.channel_store
|
||||||
|
.update(cx, |channel_store, cx| {
|
||||||
|
channel_store.set_channel_visibility(
|
||||||
|
channel_id,
|
||||||
|
ChannelVisibility::Members,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
)
|
||||||
|
.with_spacing(14.0)
|
||||||
|
.with_child(
|
||||||
|
MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
|
||||||
|
let style = theme.channel_link.style_for(state);
|
||||||
|
Label::new(format!("{}", "copy link"), style.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container.clone())
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||||
|
if let Some(channel) =
|
||||||
|
this.channel_store.read(cx).channel_for_id(channel_id)
|
||||||
|
{
|
||||||
|
let item = ClipboardItem::new(channel.link());
|
||||||
|
cx.write_to_clipboard(item);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand),
|
||||||
|
)
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::column()
|
Flex::column()
|
||||||
|
@ -190,6 +272,7 @@ impl View for ChannelModal {
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.title.container.clone()),
|
.with_style(theme.title.container.clone()),
|
||||||
)
|
)
|
||||||
|
.with_child(render_visibility(channel.id, channel.visibility, theme, cx))
|
||||||
.with_child(Flex::row().with_children([
|
.with_child(Flex::row().with_children([
|
||||||
render_mode_button::<InviteMembers>(
|
render_mode_button::<InviteMembers>(
|
||||||
Mode::InviteMembers,
|
Mode::InviteMembers,
|
||||||
|
@ -343,9 +426,11 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
if let Some((selected_user, role)) = self.user_at_index(self.selected_index) {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
|
Mode::ManageMembers => {
|
||||||
|
self.show_context_menu(role.unwrap_or(ChannelRole::Member), cx)
|
||||||
|
}
|
||||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||||
Some(proto::channel_member::Kind::Invitee) => {
|
Some(proto::channel_member::Kind::Invitee) => {
|
||||||
self.remove_selected_member(cx);
|
self.remove_selected_member(cx);
|
||||||
|
@ -373,7 +458,7 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
let full_theme = &theme::current(cx);
|
let full_theme = &theme::current(cx);
|
||||||
let theme = &full_theme.collab_panel.channel_modal;
|
let theme = &full_theme.collab_panel.channel_modal;
|
||||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||||
let (user, admin) = self.user_at_index(ix).unwrap();
|
let (user, role) = self.user_at_index(ix).unwrap();
|
||||||
let request_status = self.member_status(user.id, cx);
|
let request_status = self.member_status(user.id, cx);
|
||||||
|
|
||||||
let style = tabbed_modal
|
let style = tabbed_modal
|
||||||
|
@ -409,15 +494,25 @@ impl PickerDelegate for ChannelModalDelegate {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.with_children(admin.and_then(|admin| {
|
.with_children(if in_manage && role == Some(ChannelRole::Admin) {
|
||||||
(in_manage && admin).then(|| {
|
Some(
|
||||||
Label::new("Admin", theme.member_tag.text.clone())
|
Label::new("Admin", theme.member_tag.text.clone())
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.member_tag.container)
|
.with_style(theme.member_tag.container)
|
||||||
.aligned()
|
.aligned()
|
||||||
.left()
|
.left(),
|
||||||
})
|
)
|
||||||
}))
|
} else if in_manage && role == Some(ChannelRole::Guest) {
|
||||||
|
Some(
|
||||||
|
Label::new("Guest", theme.member_tag.text.clone())
|
||||||
|
.contained()
|
||||||
|
.with_style(theme.member_tag.container)
|
||||||
|
.aligned()
|
||||||
|
.left(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
.with_children({
|
.with_children({
|
||||||
let svg = match self.mode {
|
let svg = match self.mode {
|
||||||
Mode::ManageMembers => Some(
|
Mode::ManageMembers => Some(
|
||||||
|
@ -502,13 +597,13 @@ impl ChannelModalDelegate {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
|
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<ChannelRole>)> {
|
||||||
match self.mode {
|
match self.mode {
|
||||||
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||||
let channel_membership = self.members.get(*ix)?;
|
let channel_membership = self.members.get(*ix)?;
|
||||||
Some((
|
Some((
|
||||||
channel_membership.user.clone(),
|
channel_membership.user.clone(),
|
||||||
Some(channel_membership.admin),
|
Some(channel_membership.role),
|
||||||
))
|
))
|
||||||
}),
|
}),
|
||||||
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||||
|
@ -516,17 +611,21 @@ impl ChannelModalDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||||
let (user, admin) = self.user_at_index(self.selected_index)?;
|
let (user, role) = self.user_at_index(self.selected_index)?;
|
||||||
let admin = !admin.unwrap_or(false);
|
let new_role = if role == Some(ChannelRole::Admin) {
|
||||||
|
ChannelRole::Member
|
||||||
|
} else {
|
||||||
|
ChannelRole::Admin
|
||||||
|
};
|
||||||
let update = self.channel_store.update(cx, |store, cx| {
|
let update = self.channel_store.update(cx, |store, cx| {
|
||||||
store.set_member_admin(self.channel_id, user.id, admin, cx)
|
store.set_member_role(self.channel_id, user.id, new_role, cx)
|
||||||
});
|
});
|
||||||
cx.spawn(|picker, mut cx| async move {
|
cx.spawn(|picker, mut cx| async move {
|
||||||
update.await?;
|
update.await?;
|
||||||
picker.update(&mut cx, |picker, cx| {
|
picker.update(&mut cx, |picker, cx| {
|
||||||
let this = picker.delegate_mut();
|
let this = picker.delegate_mut();
|
||||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||||
member.admin = admin;
|
member.role = new_role;
|
||||||
}
|
}
|
||||||
cx.focus_self();
|
cx.focus_self();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
@ -572,25 +671,30 @@ impl ChannelModalDelegate {
|
||||||
|
|
||||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||||
store.invite_member(self.channel_id, user.id, false, cx)
|
store.invite_member(self.channel_id, user.id, ChannelRole::Member, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
cx.spawn(|this, mut cx| async move {
|
||||||
invite_member.await?;
|
invite_member.await?;
|
||||||
|
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.delegate_mut().members.push(ChannelMembership {
|
let new_member = ChannelMembership {
|
||||||
user,
|
user,
|
||||||
kind: proto::channel_member::Kind::Invitee,
|
kind: proto::channel_member::Kind::Invitee,
|
||||||
admin: false,
|
role: ChannelRole::Member,
|
||||||
});
|
};
|
||||||
|
let members = &mut this.delegate_mut().members;
|
||||||
|
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
|
||||||
|
Ok(ix) | Err(ix) => members.insert(ix, new_member),
|
||||||
|
}
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
.detach_and_log_err(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
|
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.context_menu.update(cx, |context_menu, cx| {
|
self.context_menu.update(cx, |context_menu, cx| {
|
||||||
context_menu.show(
|
context_menu.show(
|
||||||
Default::default(),
|
Default::default(),
|
||||||
|
@ -598,7 +702,7 @@ impl ChannelModalDelegate {
|
||||||
vec![
|
vec![
|
||||||
ContextMenuItem::action("Remove", RemoveMember),
|
ContextMenuItem::action("Remove", RemoveMember),
|
||||||
ContextMenuItem::action(
|
ContextMenuItem::action(
|
||||||
if user_is_admin {
|
if role == ChannelRole::Admin {
|
||||||
"Make non-admin"
|
"Make non-admin"
|
||||||
} else {
|
} else {
|
||||||
"Make admin"
|
"Make admin"
|
||||||
|
|
|
@ -19,6 +19,7 @@ settings = { path = "../settings" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
zed-actions = { path = "../zed-actions" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
|
|
@ -6,8 +6,12 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||||
use std::cmp::{self, Reverse};
|
use std::cmp::{self, Reverse};
|
||||||
use util::ResultExt;
|
use util::{
|
||||||
|
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||||
|
ResultExt,
|
||||||
|
};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
use zed_actions::OpenZedURL;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(toggle_command_palette);
|
cx.add_action(toggle_command_palette);
|
||||||
|
@ -167,13 +171,22 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
let intercept_result = cx.read(|cx| {
|
let mut intercept_result = cx.read(|cx| {
|
||||||
if cx.has_global::<CommandPaletteInterceptor>() {
|
if cx.has_global::<CommandPaletteInterceptor>() {
|
||||||
cx.global::<CommandPaletteInterceptor>()(&query, cx)
|
cx.global::<CommandPaletteInterceptor>()(&query, cx)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if *RELEASE_CHANNEL == ReleaseChannel::Dev {
|
||||||
|
if parse_zed_link(&query).is_some() {
|
||||||
|
intercept_result = Some(CommandInterceptResult {
|
||||||
|
action: OpenZedURL { url: query.clone() }.boxed_clone(),
|
||||||
|
string: query.clone(),
|
||||||
|
positions: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(CommandInterceptResult {
|
if let Some(CommandInterceptResult {
|
||||||
action,
|
action,
|
||||||
string,
|
string,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use collections::HashMap;
|
use collections::{HashMap, VecDeque};
|
||||||
use editor::Editor;
|
use editor::{Editor, MoveToEnd};
|
||||||
use futures::{channel::mpsc, StreamExt};
|
use futures::{channel::mpsc, StreamExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
|
@ -11,7 +11,7 @@ use gpui::{
|
||||||
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
|
AnyElement, AppContext, Element, Entity, ModelContext, ModelHandle, Subscription, View,
|
||||||
ViewContext, ViewHandle, WeakModelHandle,
|
ViewContext, ViewHandle, WeakModelHandle,
|
||||||
};
|
};
|
||||||
use language::{Buffer, LanguageServerId, LanguageServerName};
|
use language::{LanguageServerId, LanguageServerName};
|
||||||
use lsp::IoKind;
|
use lsp::IoKind;
|
||||||
use project::{search::SearchQuery, Project};
|
use project::{search::SearchQuery, Project};
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::{borrow::Cow, sync::Arc};
|
||||||
|
@ -22,8 +22,9 @@ use workspace::{
|
||||||
ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
|
ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SEND_LINE: &str = "// Send:\n";
|
const SEND_LINE: &str = "// Send:";
|
||||||
const RECEIVE_LINE: &str = "// Receive:\n";
|
const RECEIVE_LINE: &str = "// Receive:";
|
||||||
|
const MAX_STORED_LOG_ENTRIES: usize = 2000;
|
||||||
|
|
||||||
pub struct LogStore {
|
pub struct LogStore {
|
||||||
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
|
projects: HashMap<WeakModelHandle<Project>, ProjectState>,
|
||||||
|
@ -36,24 +37,25 @@ struct ProjectState {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LanguageServerState {
|
struct LanguageServerState {
|
||||||
log_buffer: ModelHandle<Buffer>,
|
log_messages: VecDeque<String>,
|
||||||
rpc_state: Option<LanguageServerRpcState>,
|
rpc_state: Option<LanguageServerRpcState>,
|
||||||
_io_logs_subscription: Option<lsp::Subscription>,
|
_io_logs_subscription: Option<lsp::Subscription>,
|
||||||
_lsp_logs_subscription: Option<lsp::Subscription>,
|
_lsp_logs_subscription: Option<lsp::Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LanguageServerRpcState {
|
struct LanguageServerRpcState {
|
||||||
buffer: ModelHandle<Buffer>,
|
rpc_messages: VecDeque<String>,
|
||||||
last_message_kind: Option<MessageKind>,
|
last_message_kind: Option<MessageKind>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LspLogView {
|
pub struct LspLogView {
|
||||||
pub(crate) editor: ViewHandle<Editor>,
|
pub(crate) editor: ViewHandle<Editor>,
|
||||||
|
editor_subscription: Subscription,
|
||||||
log_store: ModelHandle<LogStore>,
|
log_store: ModelHandle<LogStore>,
|
||||||
current_server_id: Option<LanguageServerId>,
|
current_server_id: Option<LanguageServerId>,
|
||||||
is_showing_rpc_trace: bool,
|
is_showing_rpc_trace: bool,
|
||||||
project: ModelHandle<Project>,
|
project: ModelHandle<Project>,
|
||||||
_log_store_subscription: Subscription,
|
_log_store_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LspLogToolbarItemView {
|
pub struct LspLogToolbarItemView {
|
||||||
|
@ -122,10 +124,9 @@ impl LogStore {
|
||||||
io_tx,
|
io_tx,
|
||||||
};
|
};
|
||||||
cx.spawn_weak(|this, mut cx| async move {
|
cx.spawn_weak(|this, mut cx| async move {
|
||||||
while let Some((project, server_id, io_kind, mut message)) = io_rx.next().await {
|
while let Some((project, server_id, io_kind, message)) = io_rx.next().await {
|
||||||
if let Some(this) = this.upgrade(&cx) {
|
if let Some(this) = this.upgrade(&cx) {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
message.push('\n');
|
|
||||||
this.on_io(project, server_id, io_kind, &message, cx);
|
this.on_io(project, server_id, io_kind, &message, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -168,15 +169,13 @@ impl LogStore {
|
||||||
project: &ModelHandle<Project>,
|
project: &ModelHandle<Project>,
|
||||||
id: LanguageServerId,
|
id: LanguageServerId,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<ModelHandle<Buffer>> {
|
) -> Option<&mut LanguageServerState> {
|
||||||
let project_state = self.projects.get_mut(&project.downgrade())?;
|
let project_state = self.projects.get_mut(&project.downgrade())?;
|
||||||
let server_state = project_state.servers.entry(id).or_insert_with(|| {
|
let server_state = project_state.servers.entry(id).or_insert_with(|| {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
LanguageServerState {
|
LanguageServerState {
|
||||||
rpc_state: None,
|
rpc_state: None,
|
||||||
log_buffer: cx
|
log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
|
||||||
.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""))
|
|
||||||
.clone(),
|
|
||||||
_io_logs_subscription: None,
|
_io_logs_subscription: None,
|
||||||
_lsp_logs_subscription: None,
|
_lsp_logs_subscription: None,
|
||||||
}
|
}
|
||||||
|
@ -186,7 +185,7 @@ impl LogStore {
|
||||||
if let Some(server) = server.as_deref() {
|
if let Some(server) = server.as_deref() {
|
||||||
if server.has_notification_handler::<lsp::notification::LogMessage>() {
|
if server.has_notification_handler::<lsp::notification::LogMessage>() {
|
||||||
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
|
// Another event wants to re-add the server that was already added and subscribed to, avoid doing it again.
|
||||||
return Some(server_state.log_buffer.clone());
|
return Some(server_state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,7 +214,7 @@ impl LogStore {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
Some(server_state.log_buffer.clone())
|
Some(server_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_language_server_log(
|
fn add_language_server_log(
|
||||||
|
@ -225,24 +224,26 @@ impl LogStore {
|
||||||
message: &str,
|
message: &str,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Option<()> {
|
) -> Option<()> {
|
||||||
let buffer = match self
|
let language_server_state = match self
|
||||||
.projects
|
.projects
|
||||||
.get_mut(&project.downgrade())?
|
.get_mut(&project.downgrade())?
|
||||||
.servers
|
.servers
|
||||||
.get(&id)
|
.get_mut(&id)
|
||||||
.map(|state| state.log_buffer.clone())
|
|
||||||
{
|
{
|
||||||
Some(existing_buffer) => existing_buffer,
|
Some(existing_state) => existing_state,
|
||||||
None => self.add_language_server(&project, id, cx)?,
|
None => self.add_language_server(&project, id, cx)?,
|
||||||
};
|
};
|
||||||
buffer.update(cx, |buffer, cx| {
|
|
||||||
let len = buffer.len();
|
let log_lines = &mut language_server_state.log_messages;
|
||||||
let has_newline = message.ends_with("\n");
|
while log_lines.len() >= MAX_STORED_LOG_ENTRIES {
|
||||||
buffer.edit([(len..len, message)], None, cx);
|
log_lines.pop_front();
|
||||||
if !has_newline {
|
}
|
||||||
let len = buffer.len();
|
let message = message.trim();
|
||||||
buffer.edit([(len..len, "\n")], None, cx);
|
log_lines.push_back(message.to_string());
|
||||||
}
|
cx.emit(Event::NewServerLogEntry {
|
||||||
|
id,
|
||||||
|
entry: message.to_string(),
|
||||||
|
is_rpc: false,
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
Some(())
|
Some(())
|
||||||
|
@ -260,46 +261,32 @@ impl LogStore {
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_buffer_for_server(
|
fn server_logs(
|
||||||
&self,
|
&self,
|
||||||
project: &ModelHandle<Project>,
|
project: &ModelHandle<Project>,
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
) -> Option<ModelHandle<Buffer>> {
|
) -> Option<&VecDeque<String>> {
|
||||||
let weak_project = project.downgrade();
|
let weak_project = project.downgrade();
|
||||||
let project_state = self.projects.get(&weak_project)?;
|
let project_state = self.projects.get(&weak_project)?;
|
||||||
let server_state = project_state.servers.get(&server_id)?;
|
let server_state = project_state.servers.get(&server_id)?;
|
||||||
Some(server_state.log_buffer.clone())
|
Some(&server_state.log_messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enable_rpc_trace_for_language_server(
|
fn enable_rpc_trace_for_language_server(
|
||||||
&mut self,
|
&mut self,
|
||||||
project: &ModelHandle<Project>,
|
project: &ModelHandle<Project>,
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
cx: &mut ModelContext<Self>,
|
) -> Option<&mut LanguageServerRpcState> {
|
||||||
) -> Option<ModelHandle<Buffer>> {
|
|
||||||
let weak_project = project.downgrade();
|
let weak_project = project.downgrade();
|
||||||
let project_state = self.projects.get_mut(&weak_project)?;
|
let project_state = self.projects.get_mut(&weak_project)?;
|
||||||
let server_state = project_state.servers.get_mut(&server_id)?;
|
let server_state = project_state.servers.get_mut(&server_id)?;
|
||||||
let rpc_state = server_state.rpc_state.get_or_insert_with(|| {
|
let rpc_state = server_state
|
||||||
let language = project.read(cx).languages().language_for_name("JSON");
|
.rpc_state
|
||||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
|
.get_or_insert_with(|| LanguageServerRpcState {
|
||||||
cx.spawn_weak({
|
rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES),
|
||||||
let buffer = buffer.clone();
|
|
||||||
|_, mut cx| async move {
|
|
||||||
let language = language.await.ok();
|
|
||||||
buffer.update(&mut cx, |buffer, cx| {
|
|
||||||
buffer.set_language(language, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
LanguageServerRpcState {
|
|
||||||
buffer,
|
|
||||||
last_message_kind: None,
|
last_message_kind: None,
|
||||||
}
|
});
|
||||||
});
|
Some(rpc_state)
|
||||||
Some(rpc_state.buffer.clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable_rpc_trace_for_language_server(
|
pub fn disable_rpc_trace_for_language_server(
|
||||||
|
@ -328,7 +315,7 @@ impl LogStore {
|
||||||
IoKind::StdIn => false,
|
IoKind::StdIn => false,
|
||||||
IoKind::StdErr => {
|
IoKind::StdErr => {
|
||||||
let project = project.upgrade(cx)?;
|
let project = project.upgrade(cx)?;
|
||||||
let message = format!("stderr: {}\n", message.trim());
|
let message = format!("stderr: {}", message.trim());
|
||||||
self.add_language_server_log(&project, language_server_id, &message, cx);
|
self.add_language_server_log(&project, language_server_id, &message, cx);
|
||||||
return Some(());
|
return Some(());
|
||||||
}
|
}
|
||||||
|
@ -341,24 +328,37 @@ impl LogStore {
|
||||||
.get_mut(&language_server_id)?
|
.get_mut(&language_server_id)?
|
||||||
.rpc_state
|
.rpc_state
|
||||||
.as_mut()?;
|
.as_mut()?;
|
||||||
state.buffer.update(cx, |buffer, cx| {
|
let kind = if is_received {
|
||||||
let kind = if is_received {
|
MessageKind::Receive
|
||||||
MessageKind::Receive
|
} else {
|
||||||
} else {
|
MessageKind::Send
|
||||||
MessageKind::Send
|
};
|
||||||
|
|
||||||
|
let rpc_log_lines = &mut state.rpc_messages;
|
||||||
|
if state.last_message_kind != Some(kind) {
|
||||||
|
let line_before_message = match kind {
|
||||||
|
MessageKind::Send => SEND_LINE,
|
||||||
|
MessageKind::Receive => RECEIVE_LINE,
|
||||||
};
|
};
|
||||||
if state.last_message_kind != Some(kind) {
|
rpc_log_lines.push_back(line_before_message.to_string());
|
||||||
let len = buffer.len();
|
cx.emit(Event::NewServerLogEntry {
|
||||||
let line = match kind {
|
id: language_server_id,
|
||||||
MessageKind::Send => SEND_LINE,
|
entry: line_before_message.to_string(),
|
||||||
MessageKind::Receive => RECEIVE_LINE,
|
is_rpc: true,
|
||||||
};
|
});
|
||||||
buffer.edit([(len..len, line)], None, cx);
|
}
|
||||||
state.last_message_kind = Some(kind);
|
|
||||||
}
|
while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES {
|
||||||
let len = buffer.len();
|
rpc_log_lines.pop_front();
|
||||||
buffer.edit([(len..len, message)], None, cx);
|
}
|
||||||
|
let message = message.trim();
|
||||||
|
rpc_log_lines.push_back(message.to_string());
|
||||||
|
cx.emit(Event::NewServerLogEntry {
|
||||||
|
id: language_server_id,
|
||||||
|
entry: message.to_string(),
|
||||||
|
is_rpc: true,
|
||||||
});
|
});
|
||||||
|
cx.notify();
|
||||||
Some(())
|
Some(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,8 +374,7 @@ impl LspLogView {
|
||||||
.projects
|
.projects
|
||||||
.get(&project.downgrade())
|
.get(&project.downgrade())
|
||||||
.and_then(|project| project.servers.keys().copied().next());
|
.and_then(|project| project.servers.keys().copied().next());
|
||||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, ""));
|
let model_changes_subscription = cx.observe(&log_store, |this, store, cx| {
|
||||||
let _log_store_subscription = cx.observe(&log_store, |this, store, cx| {
|
|
||||||
(|| -> Option<()> {
|
(|| -> Option<()> {
|
||||||
let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
|
let project_state = store.read(cx).projects.get(&this.project.downgrade())?;
|
||||||
if let Some(current_lsp) = this.current_server_id {
|
if let Some(current_lsp) = this.current_server_id {
|
||||||
|
@ -411,13 +410,31 @@ impl LspLogView {
|
||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
|
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e {
|
||||||
|
Event::NewServerLogEntry { id, entry, is_rpc } => {
|
||||||
|
if log_view.current_server_id == Some(*id) {
|
||||||
|
if (*is_rpc && log_view.is_showing_rpc_trace)
|
||||||
|
|| (!*is_rpc && !log_view.is_showing_rpc_trace)
|
||||||
|
{
|
||||||
|
log_view.editor.update(cx, |editor, cx| {
|
||||||
|
editor.set_read_only(false);
|
||||||
|
editor.handle_input(entry.trim(), cx);
|
||||||
|
editor.handle_input("\n", cx);
|
||||||
|
editor.set_read_only(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx);
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
editor: Self::editor_for_buffer(project.clone(), buffer, cx),
|
editor,
|
||||||
|
editor_subscription,
|
||||||
project,
|
project,
|
||||||
log_store,
|
log_store,
|
||||||
current_server_id: None,
|
current_server_id: None,
|
||||||
is_showing_rpc_trace: false,
|
is_showing_rpc_trace: false,
|
||||||
_log_store_subscription,
|
_log_store_subscriptions: vec![model_changes_subscription, events_subscriptions],
|
||||||
};
|
};
|
||||||
if let Some(server_id) = server_id {
|
if let Some(server_id) = server_id {
|
||||||
this.show_logs_for_server(server_id, cx);
|
this.show_logs_for_server(server_id, cx);
|
||||||
|
@ -425,20 +442,19 @@ impl LspLogView {
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
||||||
fn editor_for_buffer(
|
fn editor_for_logs(
|
||||||
project: ModelHandle<Project>,
|
log_contents: String,
|
||||||
buffer: ModelHandle<Buffer>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> ViewHandle<Editor> {
|
) -> (ViewHandle<Editor>, Subscription) {
|
||||||
let editor = cx.add_view(|cx| {
|
let editor = cx.add_view(|cx| {
|
||||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
let mut editor = Editor::multi_line(None, cx);
|
||||||
|
editor.set_text(log_contents, cx);
|
||||||
|
editor.move_to_end(&MoveToEnd, cx);
|
||||||
editor.set_read_only(true);
|
editor.set_read_only(true);
|
||||||
editor.move_to_end(&Default::default(), cx);
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()))
|
let editor_subscription = cx.subscribe(&editor, |_, _, event, cx| cx.emit(event.clone()));
|
||||||
.detach();
|
(editor, editor_subscription)
|
||||||
editor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
|
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
|
||||||
|
@ -487,14 +503,17 @@ impl LspLogView {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext<Self>) {
|
||||||
let buffer = self
|
let log_contents = self
|
||||||
.log_store
|
.log_store
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.log_buffer_for_server(&self.project, server_id);
|
.server_logs(&self.project, server_id)
|
||||||
if let Some(buffer) = buffer {
|
.map(log_contents);
|
||||||
|
if let Some(log_contents) = log_contents {
|
||||||
self.current_server_id = Some(server_id);
|
self.current_server_id = Some(server_id);
|
||||||
self.is_showing_rpc_trace = false;
|
self.is_showing_rpc_trace = false;
|
||||||
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
|
let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx);
|
||||||
|
self.editor = editor;
|
||||||
|
self.editor_subscription = editor_subscription;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -504,13 +523,37 @@ impl LspLogView {
|
||||||
server_id: LanguageServerId,
|
server_id: LanguageServerId,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
let buffer = self.log_store.update(cx, |log_set, cx| {
|
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||||
log_set.enable_rpc_trace_for_language_server(&self.project, server_id, cx)
|
log_store
|
||||||
|
.enable_rpc_trace_for_language_server(&self.project, server_id)
|
||||||
|
.map(|state| log_contents(&state.rpc_messages))
|
||||||
});
|
});
|
||||||
if let Some(buffer) = buffer {
|
if let Some(rpc_log) = rpc_log {
|
||||||
self.current_server_id = Some(server_id);
|
self.current_server_id = Some(server_id);
|
||||||
self.is_showing_rpc_trace = true;
|
self.is_showing_rpc_trace = true;
|
||||||
self.editor = Self::editor_for_buffer(self.project.clone(), buffer, cx);
|
let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx);
|
||||||
|
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||||
|
editor
|
||||||
|
.read(cx)
|
||||||
|
.buffer()
|
||||||
|
.read(cx)
|
||||||
|
.as_singleton()
|
||||||
|
.expect("log buffer should be a singleton")
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.spawn_weak({
|
||||||
|
let buffer = cx.handle();
|
||||||
|
|_, mut cx| async move {
|
||||||
|
let language = language.await.ok();
|
||||||
|
buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
buffer.set_language(language, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
|
||||||
|
self.editor = editor;
|
||||||
|
self.editor_subscription = editor_subscription;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -523,7 +566,7 @@ impl LspLogView {
|
||||||
) {
|
) {
|
||||||
self.log_store.update(cx, |log_store, cx| {
|
self.log_store.update(cx, |log_store, cx| {
|
||||||
if enabled {
|
if enabled {
|
||||||
log_store.enable_rpc_trace_for_language_server(&self.project, server_id, cx);
|
log_store.enable_rpc_trace_for_language_server(&self.project, server_id);
|
||||||
} else {
|
} else {
|
||||||
log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
|
log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx);
|
||||||
}
|
}
|
||||||
|
@ -535,6 +578,16 @@ impl LspLogView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_contents(lines: &VecDeque<String>) -> String {
|
||||||
|
let (a, b) = lines.as_slices();
|
||||||
|
let log_contents = a.join("\n");
|
||||||
|
if b.is_empty() {
|
||||||
|
log_contents
|
||||||
|
} else {
|
||||||
|
log_contents + "\n" + &b.join("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl View for LspLogView {
|
impl View for LspLogView {
|
||||||
fn ui_name() -> &'static str {
|
fn ui_name() -> &'static str {
|
||||||
"LspLogView"
|
"LspLogView"
|
||||||
|
@ -685,6 +738,7 @@ impl View for LspLogToolbarItemView {
|
||||||
});
|
});
|
||||||
let server_selected = current_server.is_some();
|
let server_selected = current_server.is_some();
|
||||||
|
|
||||||
|
enum LspLogScroll {}
|
||||||
enum Menu {}
|
enum Menu {}
|
||||||
let lsp_menu = Stack::new()
|
let lsp_menu = Stack::new()
|
||||||
.with_child(Self::render_language_server_menu_header(
|
.with_child(Self::render_language_server_menu_header(
|
||||||
|
@ -697,7 +751,7 @@ impl View for LspLogToolbarItemView {
|
||||||
Overlay::new(
|
Overlay::new(
|
||||||
MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
|
MouseEventHandler::new::<Menu, _>(0, cx, move |_, cx| {
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.scrollable::<Self>(0, None, cx)
|
.scrollable::<LspLogScroll>(0, None, cx)
|
||||||
.with_children(menu_rows.into_iter().map(|row| {
|
.with_children(menu_rows.into_iter().map(|row| {
|
||||||
Self::render_language_server_menu_item(
|
Self::render_language_server_menu_item(
|
||||||
row.server_id,
|
row.server_id,
|
||||||
|
@ -876,6 +930,7 @@ impl LspLogToolbarItemView {
|
||||||
) -> impl Element<Self> {
|
) -> impl Element<Self> {
|
||||||
enum ActivateLog {}
|
enum ActivateLog {}
|
||||||
enum ActivateRpcTrace {}
|
enum ActivateRpcTrace {}
|
||||||
|
enum LanguageServerCheckbox {}
|
||||||
|
|
||||||
Flex::column()
|
Flex::column()
|
||||||
.with_child({
|
.with_child({
|
||||||
|
@ -921,7 +976,7 @@ impl LspLogToolbarItemView {
|
||||||
.with_height(theme.toolbar_dropdown_menu.row_height),
|
.with_height(theme.toolbar_dropdown_menu.row_height),
|
||||||
)
|
)
|
||||||
.with_child(
|
.with_child(
|
||||||
ui::checkbox_with_label::<Self, _, Self, _>(
|
ui::checkbox_with_label::<LanguageServerCheckbox, _, Self, _>(
|
||||||
Empty::new(),
|
Empty::new(),
|
||||||
&theme.welcome.checkbox,
|
&theme.welcome.checkbox,
|
||||||
rpc_trace_enabled,
|
rpc_trace_enabled,
|
||||||
|
@ -947,8 +1002,16 @@ impl LspLogToolbarItemView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum Event {
|
||||||
|
NewServerLogEntry {
|
||||||
|
id: LanguageServerId,
|
||||||
|
entry: String,
|
||||||
|
is_rpc: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
impl Entity for LogStore {
|
impl Entity for LogStore {
|
||||||
type Event = ();
|
type Event = Event;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entity for LspLogView {
|
impl Entity for LspLogView {
|
||||||
|
|
|
@ -2027,11 +2027,16 @@ impl LocalSnapshot {
|
||||||
|
|
||||||
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
|
fn ignore_stack_for_abs_path(&self, abs_path: &Path, is_dir: bool) -> Arc<IgnoreStack> {
|
||||||
let mut new_ignores = Vec::new();
|
let mut new_ignores = Vec::new();
|
||||||
for ancestor in abs_path.ancestors().skip(1) {
|
for (index, ancestor) in abs_path.ancestors().enumerate() {
|
||||||
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
|
if index > 0 {
|
||||||
new_ignores.push((ancestor, Some(ignore.clone())));
|
if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
|
||||||
} else {
|
new_ignores.push((ancestor, Some(ignore.clone())));
|
||||||
new_ignores.push((ancestor, None));
|
} else {
|
||||||
|
new_ignores.push((ancestor, None));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ancestor.join(&*DOT_GIT).is_dir() {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2048,7 +2053,6 @@ impl LocalSnapshot {
|
||||||
if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
|
if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
|
||||||
ignore_stack = IgnoreStack::all();
|
ignore_stack = IgnoreStack::all();
|
||||||
}
|
}
|
||||||
|
|
||||||
ignore_stack
|
ignore_stack
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3064,14 +3068,21 @@ impl BackgroundScanner {
|
||||||
|
|
||||||
// Populate ignores above the root.
|
// Populate ignores above the root.
|
||||||
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
|
||||||
for ancestor in root_abs_path.ancestors().skip(1) {
|
for (index, ancestor) in root_abs_path.ancestors().enumerate() {
|
||||||
if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
|
if index != 0 {
|
||||||
{
|
if let Ok(ignore) =
|
||||||
self.state
|
build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
|
||||||
.lock()
|
{
|
||||||
.snapshot
|
self.state
|
||||||
.ignores_by_parent_abs_path
|
.lock()
|
||||||
.insert(ancestor.into(), (ignore.into(), false));
|
.snapshot
|
||||||
|
.ignores_by_parent_abs_path
|
||||||
|
.insert(ancestor.into(), (ignore.into(), false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ancestor.join(&*DOT_GIT).is_dir() {
|
||||||
|
// Reached root of git repository.
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -146,7 +146,7 @@ message Envelope {
|
||||||
DeleteChannel delete_channel = 120;
|
DeleteChannel delete_channel = 120;
|
||||||
GetChannelMembers get_channel_members = 121;
|
GetChannelMembers get_channel_members = 121;
|
||||||
GetChannelMembersResponse get_channel_members_response = 122;
|
GetChannelMembersResponse get_channel_members_response = 122;
|
||||||
SetChannelMemberAdmin set_channel_member_admin = 123;
|
SetChannelMemberRole set_channel_member_role = 123;
|
||||||
RenameChannel rename_channel = 124;
|
RenameChannel rename_channel = 124;
|
||||||
RenameChannelResponse rename_channel_response = 125;
|
RenameChannelResponse rename_channel_response = 125;
|
||||||
|
|
||||||
|
@ -174,12 +174,13 @@ message Envelope {
|
||||||
LinkChannel link_channel = 145;
|
LinkChannel link_channel = 145;
|
||||||
UnlinkChannel unlink_channel = 146;
|
UnlinkChannel unlink_channel = 146;
|
||||||
MoveChannel move_channel = 147;
|
MoveChannel move_channel = 147;
|
||||||
|
SetChannelVisibility set_channel_visibility = 148;
|
||||||
|
|
||||||
NewNotification new_notification = 148;
|
NewNotification new_notification = 149;
|
||||||
GetNotifications get_notifications = 149;
|
GetNotifications get_notifications = 150;
|
||||||
GetNotificationsResponse get_notifications_response = 150;
|
GetNotificationsResponse get_notifications_response = 151;
|
||||||
DeleteNotification delete_notification = 151;
|
DeleteNotification delete_notification = 152;
|
||||||
MarkNotificationsRead mark_notifications_read = 152; // Current max
|
MarkNotificationsRead mark_notifications_read = 153; // Current max
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -999,7 +1000,7 @@ message ChannelEdge {
|
||||||
|
|
||||||
message ChannelPermission {
|
message ChannelPermission {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
bool is_admin = 2;
|
ChannelRole role = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ChannelParticipants {
|
message ChannelParticipants {
|
||||||
|
@ -1025,8 +1026,8 @@ message GetChannelMembersResponse {
|
||||||
|
|
||||||
message ChannelMember {
|
message ChannelMember {
|
||||||
uint64 user_id = 1;
|
uint64 user_id = 1;
|
||||||
bool admin = 2;
|
|
||||||
Kind kind = 3;
|
Kind kind = 3;
|
||||||
|
ChannelRole role = 4;
|
||||||
|
|
||||||
enum Kind {
|
enum Kind {
|
||||||
Member = 0;
|
Member = 0;
|
||||||
|
@ -1048,7 +1049,7 @@ message CreateChannelResponse {
|
||||||
message InviteChannelMember {
|
message InviteChannelMember {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
uint64 user_id = 2;
|
uint64 user_id = 2;
|
||||||
bool admin = 3;
|
ChannelRole role = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RemoveChannelMember {
|
message RemoveChannelMember {
|
||||||
|
@ -1056,10 +1057,22 @@ message RemoveChannelMember {
|
||||||
uint64 user_id = 2;
|
uint64 user_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SetChannelMemberAdmin {
|
enum ChannelRole {
|
||||||
|
Admin = 0;
|
||||||
|
Member = 1;
|
||||||
|
Guest = 2;
|
||||||
|
Banned = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetChannelMemberRole {
|
||||||
uint64 channel_id = 1;
|
uint64 channel_id = 1;
|
||||||
uint64 user_id = 2;
|
uint64 user_id = 2;
|
||||||
bool admin = 3;
|
ChannelRole role = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetChannelVisibility {
|
||||||
|
uint64 channel_id = 1;
|
||||||
|
ChannelVisibility visibility = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message RenameChannel {
|
message RenameChannel {
|
||||||
|
@ -1563,9 +1576,15 @@ message Nonce {
|
||||||
uint64 lower_half = 2;
|
uint64 lower_half = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ChannelVisibility {
|
||||||
|
Public = 0;
|
||||||
|
Members = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message Channel {
|
message Channel {
|
||||||
uint64 id = 1;
|
uint64 id = 1;
|
||||||
string name = 2;
|
string name = 2;
|
||||||
|
ChannelVisibility visibility = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
message Contact {
|
message Contact {
|
||||||
|
|
|
@ -249,11 +249,12 @@ messages!(
|
||||||
(RespondToContactRequest, Foreground),
|
(RespondToContactRequest, Foreground),
|
||||||
(RoomUpdated, Foreground),
|
(RoomUpdated, Foreground),
|
||||||
(SaveBuffer, Foreground),
|
(SaveBuffer, Foreground),
|
||||||
|
(SetChannelMemberRole, Foreground),
|
||||||
|
(SetChannelVisibility, Foreground),
|
||||||
(SearchProject, Background),
|
(SearchProject, Background),
|
||||||
(SearchProjectResponse, Background),
|
(SearchProjectResponse, Background),
|
||||||
(SendChannelMessage, Background),
|
(SendChannelMessage, Background),
|
||||||
(SendChannelMessageResponse, Background),
|
(SendChannelMessageResponse, Background),
|
||||||
(SetChannelMemberAdmin, Foreground),
|
|
||||||
(ShareProject, Foreground),
|
(ShareProject, Foreground),
|
||||||
(ShareProjectResponse, Foreground),
|
(ShareProjectResponse, Foreground),
|
||||||
(ShowContacts, Foreground),
|
(ShowContacts, Foreground),
|
||||||
|
@ -356,7 +357,8 @@ request_messages!(
|
||||||
(SaveBuffer, BufferSaved),
|
(SaveBuffer, BufferSaved),
|
||||||
(SearchProject, SearchProjectResponse),
|
(SearchProject, SearchProjectResponse),
|
||||||
(SendChannelMessage, SendChannelMessageResponse),
|
(SendChannelMessage, SendChannelMessageResponse),
|
||||||
(SetChannelMemberAdmin, Ack),
|
(SetChannelMemberRole, Ack),
|
||||||
|
(SetChannelVisibility, Ack),
|
||||||
(ShareProject, ShareProjectResponse),
|
(ShareProject, ShareProjectResponse),
|
||||||
(SynchronizeBuffers, SynchronizeBuffersResponse),
|
(SynchronizeBuffers, SynchronizeBuffersResponse),
|
||||||
(Test, Test),
|
(Test, Test),
|
||||||
|
|
|
@ -537,6 +537,7 @@ impl BufferSearchBar {
|
||||||
self.active_searchable_item
|
self.active_searchable_item
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|searchable_item| searchable_item.query_suggestion(cx))
|
.map(|searchable_item| searchable_item.query_suggestion(cx))
|
||||||
|
.filter(|suggestion| !suggestion.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
|
pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
|
||||||
|
|
|
@ -287,6 +287,8 @@ pub struct TabbedModal {
|
||||||
pub header: ContainerStyle,
|
pub header: ContainerStyle,
|
||||||
pub body: ContainerStyle,
|
pub body: ContainerStyle,
|
||||||
pub title: ContainedText,
|
pub title: ContainedText,
|
||||||
|
pub visibility_toggle: Interactive<ContainedText>,
|
||||||
|
pub channel_link: Interactive<ContainedText>,
|
||||||
pub picker: Picker,
|
pub picker: Picker,
|
||||||
pub max_height: f32,
|
pub max_height: f32,
|
||||||
pub max_width: f32,
|
pub max_width: f32,
|
||||||
|
@ -1216,6 +1218,15 @@ pub struct InlineAssistantStyle {
|
||||||
pub disabled_editor: FieldEditor,
|
pub disabled_editor: FieldEditor,
|
||||||
pub pending_edit_background: Color,
|
pub pending_edit_background: Color,
|
||||||
pub include_conversation: ToggleIconButtonStyle,
|
pub include_conversation: ToggleIconButtonStyle,
|
||||||
|
pub retrieve_context: ToggleIconButtonStyle,
|
||||||
|
pub context_status: ContextStatusStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
pub struct ContextStatusStyle {
|
||||||
|
pub error_icon: Icon,
|
||||||
|
pub in_progress_icon: Icon,
|
||||||
|
pub complete_icon: Icon,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
#[derive(Clone, Deserialize, Default, JsonSchema)]
|
||||||
|
|
|
@ -289,6 +289,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
cx.add_global_action(restart);
|
cx.add_global_action(restart);
|
||||||
cx.add_async_action(Workspace::save_all);
|
cx.add_async_action(Workspace::save_all);
|
||||||
cx.add_action(Workspace::add_folder_to_project);
|
cx.add_action(Workspace::add_folder_to_project);
|
||||||
|
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
||||||
let pane = workspace.active_pane().clone();
|
let pane = workspace.active_pane().clone();
|
||||||
|
|
|
@ -8,3 +8,4 @@ publish = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gpui = { path = "../gpui" }
|
gpui = { path = "../gpui" }
|
||||||
|
serde.workspace = true
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
use gpui::actions;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gpui::{actions, impl_actions};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
actions!(
|
actions!(
|
||||||
zed,
|
zed,
|
||||||
|
@ -26,3 +29,13 @@ actions!(
|
||||||
ResetDatabase,
|
ResetDatabase,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone, PartialEq)]
|
||||||
|
pub struct OpenBrowser {
|
||||||
|
pub url: Arc<str>,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Clone, PartialEq)]
|
||||||
|
pub struct OpenZedURL {
|
||||||
|
pub url: String,
|
||||||
|
}
|
||||||
|
impl_actions!(zed, [OpenBrowser, OpenZedURL]);
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.109.0"
|
version = "0.110.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
BIN
crates/zed/contents/dev/embedded.provisionprofile
Normal file
BIN
crates/zed/contents/dev/embedded.provisionprofile
Normal file
Binary file not shown.
BIN
crates/zed/contents/preview/embedded.provisionprofile
Normal file
BIN
crates/zed/contents/preview/embedded.provisionprofile
Normal file
Binary file not shown.
Binary file not shown.
|
@ -2,6 +2,8 @@
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>com.apple.developer.associated-domains</key>
|
||||||
|
<array><string>applinks:zed.dev</string></array>
|
||||||
<key>com.apple.security.automation.apple-events</key>
|
<key>com.apple.security.automation.apple-events</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
@ -10,14 +12,8 @@
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.device.camera</key>
|
<key>com.apple.security.device.camera</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.personal-information.addressbook</key>
|
<key>com.apple.security.keychain-access-groups</key>
|
||||||
<true/>
|
<array><string>MQ55VZLNZQ.dev.zed.Shared</string></array>
|
||||||
<key>com.apple.security.personal-information.calendars</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.personal-information.location</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.personal-information.photos-library</key>
|
|
||||||
<true/>
|
|
||||||
<!-- <key>com.apple.security.cs.disable-library-validation</key>
|
<!-- <key>com.apple.security.cs.disable-library-validation</key>
|
||||||
<true/> -->
|
<true/> -->
|
||||||
</dict>
|
</dict>
|
||||||
|
|
|
@ -3,22 +3,16 @@
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use backtrace::Backtrace;
|
use backtrace::Backtrace;
|
||||||
use cli::{
|
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
|
||||||
ipc::{self, IpcSender},
|
|
||||||
CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME,
|
|
||||||
};
|
|
||||||
use client::{
|
use client::{
|
||||||
self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
self, Client, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN,
|
||||||
};
|
};
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
use editor::Editor;
|
||||||
use futures::{
|
use futures::StreamExt;
|
||||||
channel::{mpsc, oneshot},
|
|
||||||
FutureExt, SinkExt, StreamExt,
|
|
||||||
};
|
|
||||||
use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
|
use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task};
|
||||||
use isahc::{config::Configurable, Request};
|
use isahc::{config::Configurable, Request};
|
||||||
use language::{LanguageRegistry, Point};
|
use language::LanguageRegistry;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use node_runtime::RealNodeRuntime;
|
use node_runtime::RealNodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -28,7 +22,6 @@ use settings::{default_settings, handle_settings_file_changes, watch_config_file
|
||||||
use simplelog::ConfigBuilder;
|
use simplelog::ConfigBuilder;
|
||||||
use smol::process::Command;
|
use smol::process::Command;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
|
||||||
env,
|
env,
|
||||||
ffi::OsStr,
|
ffi::OsStr,
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
|
@ -42,11 +35,9 @@ use std::{
|
||||||
thread,
|
thread,
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
use sum_tree::Bias;
|
|
||||||
use util::{
|
use util::{
|
||||||
channel::{parse_zed_link, ReleaseChannel},
|
channel::{parse_zed_link, ReleaseChannel},
|
||||||
http::{self, HttpClient},
|
http::{self, HttpClient},
|
||||||
paths::PathLikeWithPosition,
|
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||||
|
@ -58,12 +49,9 @@ use zed::{
|
||||||
assets::Assets,
|
assets::Assets,
|
||||||
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||||
only_instance::{ensure_only_instance, IsOnlyInstance},
|
only_instance::{ensure_only_instance, IsOnlyInstance},
|
||||||
|
open_listener::{handle_cli_connection, OpenListener, OpenRequest},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::open_listener::{OpenListener, OpenRequest};
|
|
||||||
|
|
||||||
mod open_listener;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let http = http::client();
|
let http = http::client();
|
||||||
init_paths();
|
init_paths();
|
||||||
|
@ -113,6 +101,7 @@ fn main() {
|
||||||
|
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
cx.set_global(*RELEASE_CHANNEL);
|
cx.set_global(*RELEASE_CHANNEL);
|
||||||
|
cx.set_global(listener.clone());
|
||||||
|
|
||||||
let mut store = SettingsStore::default();
|
let mut store = SettingsStore::default();
|
||||||
store
|
store
|
||||||
|
@ -736,189 +725,6 @@ async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()>
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
|
fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
|
||||||
|
|
||||||
fn connect_to_cli(
|
|
||||||
server_name: &str,
|
|
||||||
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
|
||||||
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
|
||||||
.context("error connecting to cli")?;
|
|
||||||
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
|
|
||||||
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
|
|
||||||
|
|
||||||
handshake_tx
|
|
||||||
.send(IpcHandshake {
|
|
||||||
requests: request_tx,
|
|
||||||
responses: response_rx,
|
|
||||||
})
|
|
||||||
.context("error sending ipc handshake")?;
|
|
||||||
|
|
||||||
let (mut async_request_tx, async_request_rx) =
|
|
||||||
futures::channel::mpsc::channel::<CliRequest>(16);
|
|
||||||
thread::spawn(move || {
|
|
||||||
while let Ok(cli_request) = request_rx.recv() {
|
|
||||||
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok::<_, anyhow::Error>(())
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok((async_request_rx, response_tx))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_cli_connection(
|
|
||||||
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
|
|
||||||
app_state: Arc<AppState>,
|
|
||||||
mut cx: AsyncAppContext,
|
|
||||||
) {
|
|
||||||
if let Some(request) = requests.next().await {
|
|
||||||
match request {
|
|
||||||
CliRequest::Open { paths, wait } => {
|
|
||||||
let mut caret_positions = HashMap::new();
|
|
||||||
|
|
||||||
let paths = if paths.is_empty() {
|
|
||||||
workspace::last_opened_workspace_paths()
|
|
||||||
.await
|
|
||||||
.map(|location| location.paths().to_vec())
|
|
||||||
.unwrap_or_default()
|
|
||||||
} else {
|
|
||||||
paths
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|path_with_position_string| {
|
|
||||||
let path_with_position = PathLikeWithPosition::parse_str(
|
|
||||||
&path_with_position_string,
|
|
||||||
|path_str| {
|
|
||||||
Ok::<_, std::convert::Infallible>(
|
|
||||||
Path::new(path_str).to_path_buf(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("Infallible");
|
|
||||||
let path = path_with_position.path_like;
|
|
||||||
if let Some(row) = path_with_position.row {
|
|
||||||
if path.is_file() {
|
|
||||||
let row = row.saturating_sub(1);
|
|
||||||
let col =
|
|
||||||
path_with_position.column.unwrap_or(0).saturating_sub(1);
|
|
||||||
caret_positions.insert(path.clone(), Point::new(row, col));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(path)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut errored = false;
|
|
||||||
match cx
|
|
||||||
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok((workspace, items)) => {
|
|
||||||
let mut item_release_futures = Vec::new();
|
|
||||||
|
|
||||||
for (item, path) in items.into_iter().zip(&paths) {
|
|
||||||
match item {
|
|
||||||
Some(Ok(item)) => {
|
|
||||||
if let Some(point) = caret_positions.remove(path) {
|
|
||||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
|
||||||
active_editor
|
|
||||||
.downgrade()
|
|
||||||
.update(&mut cx, |editor, cx| {
|
|
||||||
let snapshot =
|
|
||||||
editor.snapshot(cx).display_snapshot;
|
|
||||||
let point = snapshot
|
|
||||||
.buffer_snapshot
|
|
||||||
.clip_point(point, Bias::Left);
|
|
||||||
editor.change_selections(
|
|
||||||
Some(Autoscroll::center()),
|
|
||||||
cx,
|
|
||||||
|s| s.select_ranges([point..point]),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let released = oneshot::channel();
|
|
||||||
cx.update(|cx| {
|
|
||||||
item.on_release(
|
|
||||||
cx,
|
|
||||||
Box::new(move |_| {
|
|
||||||
let _ = released.0.send(());
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.detach();
|
|
||||||
});
|
|
||||||
item_release_futures.push(released.1);
|
|
||||||
}
|
|
||||||
Some(Err(err)) => {
|
|
||||||
responses
|
|
||||||
.send(CliResponse::Stderr {
|
|
||||||
message: format!("error opening {:?}: {}", path, err),
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
errored = true;
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if wait {
|
|
||||||
let background = cx.background();
|
|
||||||
let wait = async move {
|
|
||||||
if paths.is_empty() {
|
|
||||||
let (done_tx, done_rx) = oneshot::channel();
|
|
||||||
if let Some(workspace) = workspace.upgrade(&cx) {
|
|
||||||
let _subscription = cx.update(|cx| {
|
|
||||||
cx.observe_release(&workspace, move |_, _| {
|
|
||||||
let _ = done_tx.send(());
|
|
||||||
})
|
|
||||||
});
|
|
||||||
drop(workspace);
|
|
||||||
let _ = done_rx.await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let _ =
|
|
||||||
futures::future::try_join_all(item_release_futures).await;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
.fuse();
|
|
||||||
futures::pin_mut!(wait);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
// Repeatedly check if CLI is still open to avoid wasting resources
|
|
||||||
// waiting for files or workspaces to close.
|
|
||||||
let mut timer = background.timer(Duration::from_secs(1)).fuse();
|
|
||||||
futures::select_biased! {
|
|
||||||
_ = wait => break,
|
|
||||||
_ = timer => {
|
|
||||||
if responses.send(CliResponse::Ping).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
errored = true;
|
|
||||||
responses
|
|
||||||
.send(CliResponse::Stderr {
|
|
||||||
message: format!("error opening {:?}: {}", paths, error),
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
responses
|
|
||||||
.send(CliResponse::Exit {
|
|
||||||
status: i32::from(errored),
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
||||||
&[
|
&[
|
||||||
("Go to file", &file_finder::Toggle),
|
("Go to file", &file_finder::Toggle),
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
use anyhow::anyhow;
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use cli::{ipc, IpcHandshake};
|
||||||
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
||||||
use futures::channel::mpsc;
|
use editor::scroll::autoscroll::Autoscroll;
|
||||||
|
use editor::Editor;
|
||||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
|
use futures::channel::{mpsc, oneshot};
|
||||||
|
use futures::{FutureExt, SinkExt, StreamExt};
|
||||||
|
use gpui::AsyncAppContext;
|
||||||
|
use language::{Bias, Point};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::os::unix::prelude::OsStrExt;
|
use std::os::unix::prelude::OsStrExt;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
use std::{path::PathBuf, sync::atomic::AtomicBool};
|
use std::{path::PathBuf, sync::atomic::AtomicBool};
|
||||||
use util::channel::parse_zed_link;
|
use util::channel::parse_zed_link;
|
||||||
|
use util::paths::PathLikeWithPosition;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
use workspace::AppState;
|
||||||
use crate::connect_to_cli;
|
|
||||||
|
|
||||||
pub enum OpenRequest {
|
pub enum OpenRequest {
|
||||||
Paths {
|
Paths {
|
||||||
|
@ -96,3 +107,186 @@ impl OpenListener {
|
||||||
Some(OpenRequest::Paths { paths })
|
Some(OpenRequest::Paths { paths })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn connect_to_cli(
|
||||||
|
server_name: &str,
|
||||||
|
) -> Result<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)> {
|
||||||
|
let handshake_tx = cli::ipc::IpcSender::<IpcHandshake>::connect(server_name.to_string())
|
||||||
|
.context("error connecting to cli")?;
|
||||||
|
let (request_tx, request_rx) = ipc::channel::<CliRequest>()?;
|
||||||
|
let (response_tx, response_rx) = ipc::channel::<CliResponse>()?;
|
||||||
|
|
||||||
|
handshake_tx
|
||||||
|
.send(IpcHandshake {
|
||||||
|
requests: request_tx,
|
||||||
|
responses: response_rx,
|
||||||
|
})
|
||||||
|
.context("error sending ipc handshake")?;
|
||||||
|
|
||||||
|
let (mut async_request_tx, async_request_rx) =
|
||||||
|
futures::channel::mpsc::channel::<CliRequest>(16);
|
||||||
|
thread::spawn(move || {
|
||||||
|
while let Ok(cli_request) = request_rx.recv() {
|
||||||
|
if smol::block_on(async_request_tx.send(cli_request)).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((async_request_rx, response_tx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_cli_connection(
|
||||||
|
(mut requests, responses): (mpsc::Receiver<CliRequest>, IpcSender<CliResponse>),
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) {
|
||||||
|
if let Some(request) = requests.next().await {
|
||||||
|
match request {
|
||||||
|
CliRequest::Open { paths, wait } => {
|
||||||
|
let mut caret_positions = HashMap::new();
|
||||||
|
|
||||||
|
let paths = if paths.is_empty() {
|
||||||
|
workspace::last_opened_workspace_paths()
|
||||||
|
.await
|
||||||
|
.map(|location| location.paths().to_vec())
|
||||||
|
.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|path_with_position_string| {
|
||||||
|
let path_with_position = PathLikeWithPosition::parse_str(
|
||||||
|
&path_with_position_string,
|
||||||
|
|path_str| {
|
||||||
|
Ok::<_, std::convert::Infallible>(
|
||||||
|
Path::new(path_str).to_path_buf(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.expect("Infallible");
|
||||||
|
let path = path_with_position.path_like;
|
||||||
|
if let Some(row) = path_with_position.row {
|
||||||
|
if path.is_file() {
|
||||||
|
let row = row.saturating_sub(1);
|
||||||
|
let col =
|
||||||
|
path_with_position.column.unwrap_or(0).saturating_sub(1);
|
||||||
|
caret_positions.insert(path.clone(), Point::new(row, col));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(path)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut errored = false;
|
||||||
|
match cx
|
||||||
|
.update(|cx| workspace::open_paths(&paths, &app_state, None, cx))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((workspace, items)) => {
|
||||||
|
let mut item_release_futures = Vec::new();
|
||||||
|
|
||||||
|
for (item, path) in items.into_iter().zip(&paths) {
|
||||||
|
match item {
|
||||||
|
Some(Ok(item)) => {
|
||||||
|
if let Some(point) = caret_positions.remove(path) {
|
||||||
|
if let Some(active_editor) = item.downcast::<Editor>() {
|
||||||
|
active_editor
|
||||||
|
.downgrade()
|
||||||
|
.update(&mut cx, |editor, cx| {
|
||||||
|
let snapshot =
|
||||||
|
editor.snapshot(cx).display_snapshot;
|
||||||
|
let point = snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.clip_point(point, Bias::Left);
|
||||||
|
editor.change_selections(
|
||||||
|
Some(Autoscroll::center()),
|
||||||
|
cx,
|
||||||
|
|s| s.select_ranges([point..point]),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let released = oneshot::channel();
|
||||||
|
cx.update(|cx| {
|
||||||
|
item.on_release(
|
||||||
|
cx,
|
||||||
|
Box::new(move |_| {
|
||||||
|
let _ = released.0.send(());
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
item_release_futures.push(released.1);
|
||||||
|
}
|
||||||
|
Some(Err(err)) => {
|
||||||
|
responses
|
||||||
|
.send(CliResponse::Stderr {
|
||||||
|
message: format!("error opening {:?}: {}", path, err),
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
errored = true;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if wait {
|
||||||
|
let background = cx.background();
|
||||||
|
let wait = async move {
|
||||||
|
if paths.is_empty() {
|
||||||
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
|
if let Some(workspace) = workspace.upgrade(&cx) {
|
||||||
|
let _subscription = cx.update(|cx| {
|
||||||
|
cx.observe_release(&workspace, move |_, _| {
|
||||||
|
let _ = done_tx.send(());
|
||||||
|
})
|
||||||
|
});
|
||||||
|
drop(workspace);
|
||||||
|
let _ = done_rx.await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let _ =
|
||||||
|
futures::future::try_join_all(item_release_futures).await;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
.fuse();
|
||||||
|
futures::pin_mut!(wait);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Repeatedly check if CLI is still open to avoid wasting resources
|
||||||
|
// waiting for files or workspaces to close.
|
||||||
|
let mut timer = background.timer(Duration::from_secs(1)).fuse();
|
||||||
|
futures::select_biased! {
|
||||||
|
_ = wait => break,
|
||||||
|
_ = timer => {
|
||||||
|
if responses.send(CliResponse::Ping).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
errored = true;
|
||||||
|
responses
|
||||||
|
.send(CliResponse::Stderr {
|
||||||
|
message: format!("error opening {:?}: {}", paths, error),
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses
|
||||||
|
.send(CliResponse::Exit {
|
||||||
|
status: i32::from(errored),
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ pub mod assets;
|
||||||
pub mod languages;
|
pub mod languages;
|
||||||
pub mod menus;
|
pub mod menus;
|
||||||
pub mod only_instance;
|
pub mod only_instance;
|
||||||
|
pub mod open_listener;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ use gpui::{
|
||||||
AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
|
AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
|
||||||
};
|
};
|
||||||
pub use lsp;
|
pub use lsp;
|
||||||
|
use open_listener::OpenListener;
|
||||||
pub use project;
|
pub use project;
|
||||||
use project_panel::ProjectPanel;
|
use project_panel::ProjectPanel;
|
||||||
use quick_action_bar::QuickActionBar;
|
use quick_action_bar::QuickActionBar;
|
||||||
|
@ -87,6 +89,10 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
cx.add_global_action(quit);
|
cx.add_global_action(quit);
|
||||||
|
cx.add_global_action(move |action: &OpenZedURL, cx| {
|
||||||
|
cx.global::<Arc<OpenListener>>()
|
||||||
|
.open_urls(vec![action.url.clone()])
|
||||||
|
});
|
||||||
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url));
|
||||||
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
|
||||||
theme::adjust_font_size(cx, |size| *size += 1.0)
|
theme::adjust_font_size(cx, |size| *size += 1.0)
|
||||||
|
|
|
@ -134,6 +134,8 @@ else
|
||||||
cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/"
|
cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
cp crates/zed/contents/$channel/embedded.provisionprofile "${app_path}/Contents/"
|
||||||
|
|
||||||
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
|
if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then
|
||||||
echo "Signing bundle with Apple-issued certificate"
|
echo "Signing bundle with Apple-issued certificate"
|
||||||
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
|
security create-keychain -p "$MACOS_CERTIFICATE_PASSWORD" zed.keychain || echo ""
|
||||||
|
@ -143,14 +145,32 @@ if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTAR
|
||||||
security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
security import /tmp/zed-certificate.p12 -k zed.keychain -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||||
rm /tmp/zed-certificate.p12
|
rm /tmp/zed-certificate.p12
|
||||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CERTIFICATE_PASSWORD" zed.keychain
|
||||||
/usr/bin/codesign --force --deep --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
|
|
||||||
|
# sequence of codesign commands modeled after this example: https://developer.apple.com/forums/thread/701514
|
||||||
|
/usr/bin/codesign --force --timestamp --sign "Zed Industries, Inc." "${app_path}/Contents/Frameworks/WebRTC.framework" -v
|
||||||
|
/usr/bin/codesign --force --timestamp --options runtime --sign "Zed Industries, Inc." "${app_path}/Contents/MacOS/cli" -v
|
||||||
|
/usr/bin/codesign --force --timestamp --options runtime --entitlements crates/zed/resources/zed.entitlements --sign "Zed Industries, Inc." "${app_path}" -v
|
||||||
|
|
||||||
security default-keychain -s login.keychain
|
security default-keychain -s login.keychain
|
||||||
else
|
else
|
||||||
echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD"
|
echo "One or more of the following variables are missing: MACOS_CERTIFICATE, MACOS_CERTIFICATE_PASSWORD, APPLE_NOTARIZATION_USERNAME, APPLE_NOTARIZATION_PASSWORD"
|
||||||
echo "Performing an ad-hoc signature, but this bundle should not be distributed"
|
if [[ "$local_only" = false ]]; then
|
||||||
echo "If you see 'The application cannot be opened for an unexpected reason,' you likely don't have the necessary entitlements to run the application in your signing keychain"
|
echo "To create a self-signed local build use ./scripts/build.sh -ldf"
|
||||||
echo "You will need to download a new signing key from developer.apple.com, add it to keychain, and export MACOS_SIGNING_KEY=<email address of signing key>"
|
exit 1
|
||||||
codesign --force --deep --entitlements crates/zed/resources/zed.entitlements --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v
|
fi
|
||||||
|
|
||||||
|
echo "====== WARNING ======"
|
||||||
|
echo "This bundle is being signed without all entitlements, some features (e.g. universal links) will not work"
|
||||||
|
echo "====== WARNING ======"
|
||||||
|
|
||||||
|
# NOTE: if you need to test universal links you have a few paths forward:
|
||||||
|
# - create a PR and tag it with the `run-build-dmg` label, and download the .dmg file from there.
|
||||||
|
# - get a signing key for the MQ55VZLNZQ team from Nathan.
|
||||||
|
# - create your own signing key, and update references to MQ55VZLNZQ to your own team ID
|
||||||
|
# then comment out this line.
|
||||||
|
cat crates/zed/resources/zed.entitlements | sed '/com.apple.developer.associated-domains/,+1d' > "${app_path}/Contents/Resources/zed.entitlements"
|
||||||
|
|
||||||
|
codesign --force --deep --entitlements "${app_path}/Contents/Resources/zed.entitlements" --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$target_dir" = "debug" && "$local_only" = false ]]; then
|
if [[ "$target_dir" = "debug" && "$local_only" = false ]]; then
|
||||||
|
|
|
@ -79,6 +79,80 @@ export default function assistant(): any {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pending_edit_background: background(theme.highest, "positive"),
|
pending_edit_background: background(theme.highest, "positive"),
|
||||||
|
context_status: {
|
||||||
|
error_icon: {
|
||||||
|
margin: { left: 8, right: 18 },
|
||||||
|
color: foreground(theme.highest, "negative"),
|
||||||
|
width: 12,
|
||||||
|
},
|
||||||
|
in_progress_icon: {
|
||||||
|
margin: { left: 8, right: 18 },
|
||||||
|
color: foreground(theme.highest, "positive"),
|
||||||
|
width: 12,
|
||||||
|
},
|
||||||
|
complete_icon: {
|
||||||
|
margin: { left: 8, right: 18 },
|
||||||
|
color: foreground(theme.highest, "positive"),
|
||||||
|
width: 12,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
retrieve_context: toggleable({
|
||||||
|
base: interactive({
|
||||||
|
base: {
|
||||||
|
icon_size: 12,
|
||||||
|
color: foreground(theme.highest, "variant"),
|
||||||
|
|
||||||
|
button_width: 12,
|
||||||
|
background: background(theme.highest, "on"),
|
||||||
|
corner_radius: 2,
|
||||||
|
border: {
|
||||||
|
width: 1., color: background(theme.highest, "on")
|
||||||
|
},
|
||||||
|
margin: { left: 2 },
|
||||||
|
padding: {
|
||||||
|
left: 4,
|
||||||
|
right: 4,
|
||||||
|
top: 4,
|
||||||
|
bottom: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
hovered: {
|
||||||
|
...text(theme.highest, "mono", "variant", "hovered"),
|
||||||
|
background: background(theme.highest, "on", "hovered"),
|
||||||
|
border: {
|
||||||
|
width: 1., color: background(theme.highest, "on", "hovered")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
clicked: {
|
||||||
|
...text(theme.highest, "mono", "variant", "pressed"),
|
||||||
|
background: background(theme.highest, "on", "pressed"),
|
||||||
|
border: {
|
||||||
|
width: 1., color: background(theme.highest, "on", "pressed")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
state: {
|
||||||
|
active: {
|
||||||
|
default: {
|
||||||
|
icon_size: 12,
|
||||||
|
button_width: 12,
|
||||||
|
color: foreground(theme.highest, "variant"),
|
||||||
|
background: background(theme.highest, "accent"),
|
||||||
|
border: border(theme.highest, "accent"),
|
||||||
|
},
|
||||||
|
hovered: {
|
||||||
|
background: background(theme.highest, "accent", "hovered"),
|
||||||
|
border: border(theme.highest, "accent", "hovered"),
|
||||||
|
},
|
||||||
|
clicked: {
|
||||||
|
background: background(theme.highest, "accent", "pressed"),
|
||||||
|
border: border(theme.highest, "accent", "pressed"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
include_conversation: toggleable({
|
include_conversation: toggleable({
|
||||||
base: interactive({
|
base: interactive({
|
||||||
base: {
|
base: {
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { useTheme } from "../theme"
|
import { StyleSets, useTheme } from "../theme"
|
||||||
import { background, border, foreground, text } from "./components"
|
import { background, border, foreground, text } from "./components"
|
||||||
import picker from "./picker"
|
import picker from "./picker"
|
||||||
import { input } from "../component/input"
|
import { input } from "../component/input"
|
||||||
import contact_finder from "./contact_finder"
|
import contact_finder from "./contact_finder"
|
||||||
import { tab } from "../component/tab"
|
import { tab } from "../component/tab"
|
||||||
import { icon_button } from "../component/icon_button"
|
import { icon_button } from "../component/icon_button"
|
||||||
|
import { interactive } from "../element/interactive"
|
||||||
|
|
||||||
export default function channel_modal(): any {
|
export default function channel_modal(): any {
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
@ -27,6 +28,24 @@ export default function channel_modal(): any {
|
||||||
|
|
||||||
const picker_input = input()
|
const picker_input = input()
|
||||||
|
|
||||||
|
const interactive_text = (styleset: StyleSets) =>
|
||||||
|
interactive({
|
||||||
|
base: {
|
||||||
|
padding: {
|
||||||
|
left: 8,
|
||||||
|
top: 8
|
||||||
|
},
|
||||||
|
...text(theme.middle, "sans", styleset, "default"),
|
||||||
|
}, state: {
|
||||||
|
hovered: {
|
||||||
|
...text(theme.middle, "sans", styleset, "hovered"),
|
||||||
|
},
|
||||||
|
clicked: {
|
||||||
|
...text(theme.middle, "sans", styleset, "active"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const member_icon_style = icon_button({
|
const member_icon_style = icon_button({
|
||||||
variant: "ghost",
|
variant: "ghost",
|
||||||
size: "sm",
|
size: "sm",
|
||||||
|
@ -88,6 +107,8 @@ export default function channel_modal(): any {
|
||||||
left: BUTTON_OFFSET,
|
left: BUTTON_OFFSET,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
visibility_toggle: interactive_text("base"),
|
||||||
|
channel_link: interactive_text("accent"),
|
||||||
picker: {
|
picker: {
|
||||||
empty_container: {},
|
empty_container: {},
|
||||||
item: {
|
item: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue