Compare commits
21 Commits
linux/trac
...
linux-sche
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
408fe83b90 | ||
|
|
77b31d1845 | ||
|
|
15b8790a2c | ||
|
|
b693cbfcb7 | ||
|
|
73d7f70ff6 | ||
|
|
be5b7b2e70 | ||
|
|
4434353f73 | ||
|
|
95637a0320 | ||
|
|
ee623f77c1 | ||
|
|
c732865fc5 | ||
|
|
8a659af82c | ||
|
|
07dc4050bf | ||
|
|
896b9bda23 | ||
|
|
9282bf97ae | ||
|
|
33a67ad6b9 | ||
|
|
d4ddc4c62c | ||
|
|
8944af7406 | ||
|
|
1662993811 | ||
|
|
7ef64fe6db | ||
|
|
f147722fe0 | ||
|
|
e1a6efa609 |
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -319,7 +319,6 @@ jobs:
|
||||
|
||||
- name: Upload app bundle to release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
with:
|
||||
draft: true
|
||||
prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }}
|
||||
|
||||
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -377,6 +377,7 @@ dependencies = [
|
||||
"cargo_toml",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"ctor",
|
||||
@@ -419,6 +420,7 @@ dependencies = [
|
||||
"telemetry_events",
|
||||
"terminal",
|
||||
"terminal_view",
|
||||
"text",
|
||||
"theme",
|
||||
"tiktoken-rs",
|
||||
"toml 0.8.10",
|
||||
@@ -2405,6 +2407,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -2463,6 +2466,7 @@ version = "0.44.0"
|
||||
dependencies = [
|
||||
"anthropic",
|
||||
"anyhow",
|
||||
"assistant",
|
||||
"async-trait",
|
||||
"async-tungstenite",
|
||||
"audio",
|
||||
@@ -8270,6 +8274,7 @@ dependencies = [
|
||||
"assistant",
|
||||
"editor",
|
||||
"gpui",
|
||||
"repl",
|
||||
"search",
|
||||
"settings",
|
||||
"ui",
|
||||
@@ -13581,7 +13586,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.144.0"
|
||||
version = "0.145.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -518,7 +518,7 @@ single_range_in_vec_init = "allow"
|
||||
|
||||
# There are a bunch of rules currently failing in the `style` group, so
|
||||
# allow all of those, for now.
|
||||
style = "allow"
|
||||
style = { level = "allow", priority = -1 }
|
||||
|
||||
# Individual rules that have violations in the codebase:
|
||||
almost_complete_range = "allow"
|
||||
|
||||
14
assets/icons/repl_neutral.svg
Normal file
14
assets/icons/repl_neutral.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_58)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_32_58">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
20
assets/icons/repl_off.svg
Normal file
20
assets/icons/repl_off.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_39_129)">
|
||||
<path d="M22.0209 11.9553C22.0059 10.0068 21.4219 8.10512 20.3408 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.1001 2.18C11.355 1.93537 12.1493 1.93674 13.5027 2.10594" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.8198 10.1C22.0644 11.3548 22.0644 12.6451 21.8198 13.9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M20.2898 17.6C19.5716 18.6622 18.6548 19.5757 17.5898 20.29" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.9008 21.82C12.6459 22.0644 11.6432 22.1543 10.3883 21.91" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.18005 13.9C1.93543 12.6451 1.93543 11.3548 2.18005 10.1" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M3.70996 6.40002C4.42822 5.33775 5.34503 4.42433 6.40996 3.71002" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1.99072 12.0748C2.00804 14.0118 2.58758 15.9021 3.65891 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_39_129">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
15
assets/icons/repl_pause.svg
Normal file
15
assets/icons/repl_pause.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_70)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14 15V9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_32_70">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
14
assets/icons/repl_play.svg
Normal file
14
assets/icons/repl_play.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_32_64)">
|
||||
<path d="M19 7C20.1046 7 21 6.10457 21 5C21 3.89543 20.1046 3 19 3C17.8954 3 17 3.89543 17 5C17 6.10457 17.8954 7 19 7Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 21C6.10457 21 7 20.1046 7 19C7 17.8954 6.10457 17 5 17C3.89543 17 3 17.8954 3 19C3 20.1046 3.89543 21 5 21Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10.3999 21.9C12.3227 22.2159 14.2958 21.9632 16.0769 21.173C17.858 20.3827 19.3694 19.0893 20.4254 17.4517C21.4814 15.8142 22.036 13.9037 22.021 11.9553C22.006 10.0068 21.422 8.10512 20.3409 6.48401" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.4998 2.10002C11.5849 1.8076 9.62631 2.07763 7.86198 2.87732C6.09765 3.677 4.60356 4.9719 3.56126 6.60468C2.51896 8.23745 1.97332 10.1378 1.99063 12.0748C2.00795 14.0118 2.58749 15.9021 3.65882 17.516" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 8.56055C10 8.32095 10.267 8.17803 10.4664 8.31094L15.6256 11.7504C15.8037 11.8691 15.8037 12.1309 15.6256 12.2496L10.4664 15.6891C10.267 15.822 10 15.6791 10 15.4394V8.56055Z" fill="white" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_32_64">
|
||||
<rect width="24" height="24" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -569,7 +569,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && jupyter",
|
||||
"context": "Editor && jupyter && !ContextEditor",
|
||||
"bindings": {
|
||||
"cmd-enter": "repl::Run"
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"j": "vim::Down",
|
||||
"down": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"ctrl-m": "vim::NextLineStart",
|
||||
"tab": "vim::Tab",
|
||||
"shift-tab": "vim::Tab",
|
||||
"k": "vim::Up",
|
||||
|
||||
@@ -491,7 +491,7 @@
|
||||
"info": "#5c78e2ff",
|
||||
"info.background": "#e2e2faff",
|
||||
"info.border": "#cbcdf6ff",
|
||||
"modified": "#dec184ff",
|
||||
"modified": "#a47a23ff",
|
||||
"modified.background": "#faf2e6ff",
|
||||
"modified.border": "#f4e7d1ff",
|
||||
"predictive": "#9b9ec6ff",
|
||||
|
||||
@@ -12,6 +12,14 @@ workspace = true
|
||||
path = "src/assistant.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"editor/test-support",
|
||||
"language/test-support",
|
||||
"project/test-support",
|
||||
"text/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
@@ -21,6 +29,7 @@ breadcrumbs.workspace = true
|
||||
cargo_toml.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
@@ -72,7 +81,9 @@ picker.workspace = true
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
log.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod completion_provider;
|
||||
mod context_store;
|
||||
mod context;
|
||||
pub mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
@@ -16,8 +17,9 @@ use assistant_settings::{AnthropicModel, AssistantSettings, CloudModel, OllamaMo
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use context_store::*;
|
||||
pub use completion_provider::*;
|
||||
pub use context::*;
|
||||
pub use context_store::*;
|
||||
use fs::Fs;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
use indexed_docs::IndexedDocsRegistry;
|
||||
@@ -57,10 +59,14 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(
|
||||
Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
struct MessageId(usize);
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub struct MessageId(clock::Lamport);
|
||||
|
||||
impl MessageId {
|
||||
pub fn as_u64(self) -> u64 {
|
||||
self.0.as_u64()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -71,8 +77,26 @@ pub enum Role {
|
||||
}
|
||||
|
||||
impl Role {
|
||||
pub fn cycle(&mut self) {
|
||||
*self = match self {
|
||||
pub fn from_proto(role: i32) -> Role {
|
||||
match proto::LanguageModelRole::from_i32(role) {
|
||||
Some(proto::LanguageModelRole::LanguageModelUser) => Role::User,
|
||||
Some(proto::LanguageModelRole::LanguageModelAssistant) => Role::Assistant,
|
||||
Some(proto::LanguageModelRole::LanguageModelSystem) => Role::System,
|
||||
Some(proto::LanguageModelRole::LanguageModelTool) => Role::System,
|
||||
None => Role::User,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::LanguageModelRole {
|
||||
match self {
|
||||
Role::User => proto::LanguageModelRole::LanguageModelUser,
|
||||
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
|
||||
Role::System => proto::LanguageModelRole::LanguageModelSystem,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(self) -> Role {
|
||||
match self {
|
||||
Role::User => Role::Assistant,
|
||||
Role::Assistant => Role::System,
|
||||
Role::System => Role::User,
|
||||
@@ -151,11 +175,7 @@ pub struct LanguageModelRequestMessage {
|
||||
impl LanguageModelRequestMessage {
|
||||
pub fn to_proto(&self) -> proto::LanguageModelRequestMessage {
|
||||
proto::LanguageModelRequestMessage {
|
||||
role: match self.role {
|
||||
Role::User => proto::LanguageModelRole::LanguageModelUser,
|
||||
Role::Assistant => proto::LanguageModelRole::LanguageModelAssistant,
|
||||
Role::System => proto::LanguageModelRole::LanguageModelSystem,
|
||||
} as i32,
|
||||
role: self.role.to_proto() as i32,
|
||||
content: self.content.clone(),
|
||||
tool_calls: Vec::new(),
|
||||
tool_call_id: None,
|
||||
@@ -222,19 +242,48 @@ pub struct LanguageModelChoiceDelta {
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct MessageMetadata {
|
||||
role: Role,
|
||||
status: MessageStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
enum MessageStatus {
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub enum MessageStatus {
|
||||
Pending,
|
||||
Done,
|
||||
Error(SharedString),
|
||||
}
|
||||
|
||||
impl MessageStatus {
|
||||
pub fn from_proto(status: proto::ContextMessageStatus) -> MessageStatus {
|
||||
match status.variant {
|
||||
Some(proto::context_message_status::Variant::Pending(_)) => MessageStatus::Pending,
|
||||
Some(proto::context_message_status::Variant::Done(_)) => MessageStatus::Done,
|
||||
Some(proto::context_message_status::Variant::Error(error)) => {
|
||||
MessageStatus::Error(error.message.into())
|
||||
}
|
||||
None => MessageStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_proto(&self) -> proto::ContextMessageStatus {
|
||||
match self {
|
||||
MessageStatus::Pending => proto::ContextMessageStatus {
|
||||
variant: Some(proto::context_message_status::Variant::Pending(
|
||||
proto::context_message_status::Pending {},
|
||||
)),
|
||||
},
|
||||
MessageStatus::Done => proto::ContextMessageStatus {
|
||||
variant: Some(proto::context_message_status::Variant::Done(
|
||||
proto::context_message_status::Done {},
|
||||
)),
|
||||
},
|
||||
MessageStatus::Error(message) => proto::ContextMessageStatus {
|
||||
variant: Some(proto::context_message_status::Variant::Error(
|
||||
proto::context_message_status::Error {
|
||||
message: message.to_string(),
|
||||
},
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The state pertaining to the Assistant.
|
||||
#[derive(Default)]
|
||||
struct Assistant {
|
||||
@@ -287,6 +336,7 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) {
|
||||
})
|
||||
.detach();
|
||||
|
||||
context_store::init(&client);
|
||||
prompt_library::init(cx);
|
||||
completion_provider::init(client.clone(), cx);
|
||||
assistant_slash_command::init(cx);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
mod anthropic;
|
||||
mod cloud;
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
mod fake;
|
||||
mod ollama;
|
||||
mod open_ai;
|
||||
|
||||
pub use anthropic::*;
|
||||
pub use cloud::*;
|
||||
#[cfg(test)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use fake::*;
|
||||
pub use ollama::*;
|
||||
pub use open_ai::*;
|
||||
|
||||
@@ -13,7 +13,6 @@ pub struct FakeCompletionProvider {
|
||||
}
|
||||
|
||||
impl FakeCompletionProvider {
|
||||
#[cfg(test)]
|
||||
pub fn setup_test(cx: &mut AppContext) -> Self {
|
||||
use crate::CompletionProvider;
|
||||
use parking_lot::RwLock;
|
||||
|
||||
3009
crates/assistant/src/context.rs
Normal file
3009
crates/assistant/src/context.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,97 +1,117 @@
|
||||
use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandOutputSection;
|
||||
use collections::HashMap;
|
||||
use crate::{
|
||||
Context, ContextEvent, ContextId, ContextOperation, ContextVersion, SavedContext,
|
||||
SavedContextMetadata,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use client::{proto, telemetry::Telemetry, Client, TypedEnvelope};
|
||||
use clock::ReplicaId;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{AppContext, Model, ModelContext, Task};
|
||||
use gpui::{AppContext, AsyncAppContext, Context as _, Model, ModelContext, Task, WeakModel};
|
||||
use language::LanguageRegistry;
|
||||
use paths::contexts_dir;
|
||||
use project::Project;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use ui::Context;
|
||||
use std::{
|
||||
cmp::Reverse,
|
||||
ffi::OsStr,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
pub id: MessageId,
|
||||
pub start: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContext {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
pub slash_command_output_sections: Vec<SlashCommandOutputSection<usize>>,
|
||||
}
|
||||
|
||||
impl SavedContext {
|
||||
pub const VERSION: &'static str = "0.3.0";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedContextV0_2_0 {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
pub text: String,
|
||||
pub messages: Vec<SavedMessage>,
|
||||
pub message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedContextV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
text: String,
|
||||
messages: Vec<SavedMessage>,
|
||||
message_metadata: HashMap<MessageId, MessageMetadata>,
|
||||
summary: String,
|
||||
api_url: Option<String>,
|
||||
model: OpenAiModel,
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(ContextStore::handle_advertise_contexts);
|
||||
client.add_model_request_handler(ContextStore::handle_open_context);
|
||||
client.add_model_message_handler(ContextStore::handle_update_context);
|
||||
client.add_model_request_handler(ContextStore::handle_synchronize_contexts);
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
pub struct RemoteContextMetadata {
|
||||
pub id: ContextId,
|
||||
pub summary: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ContextStore {
|
||||
contexts: Vec<ContextHandle>,
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
host_contexts: Vec<RemoteContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
telemetry: Arc<Telemetry>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
client: Arc<Client>,
|
||||
project: Model<Project>,
|
||||
project_is_shared: bool,
|
||||
client_subscription: Option<client::Subscription>,
|
||||
_project_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
enum ContextHandle {
|
||||
Weak(WeakModel<Context>),
|
||||
Strong(Model<Context>),
|
||||
}
|
||||
|
||||
impl ContextHandle {
|
||||
fn upgrade(&self) -> Option<Model<Context>> {
|
||||
match self {
|
||||
ContextHandle::Weak(weak) => weak.upgrade(),
|
||||
ContextHandle::Strong(strong) => Some(strong.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakModel<Context> {
|
||||
match self {
|
||||
ContextHandle::Weak(weak) => weak.clone(),
|
||||
ContextHandle::Strong(strong) => strong.downgrade(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextStore {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
pub fn new(project: Model<Project>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let languages = project.read(cx).languages().clone();
|
||||
let telemetry = project.read(cx).client().telemetry().clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(contexts_dir(), CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
contexts_metadata: Vec::new(),
|
||||
fs,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| {
|
||||
let mut this = Self {
|
||||
contexts: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
host_contexts: Vec::new(),
|
||||
fs,
|
||||
languages,
|
||||
telemetry,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
while events.next().await.is_some() {
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
}),
|
||||
.log_err()
|
||||
}),
|
||||
client_subscription: None,
|
||||
_project_subscriptions: vec![
|
||||
cx.observe(&project, Self::handle_project_changed),
|
||||
cx.subscribe(&project, Self::handle_project_event),
|
||||
],
|
||||
project_is_shared: false,
|
||||
client: project.read(cx).client(),
|
||||
project: project.clone(),
|
||||
};
|
||||
this.handle_project_changed(project, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
this
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| this.reload(cx))?
|
||||
.await
|
||||
@@ -100,54 +120,433 @@ impl ContextStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
|
||||
async fn handle_advertise_contexts(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::AdvertiseContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.host_contexts = envelope
|
||||
.payload
|
||||
.contexts
|
||||
.into_iter()
|
||||
.map(|context| RemoteContextMetadata {
|
||||
id: ContextId::from_proto(context.context_id),
|
||||
summary: context.summary,
|
||||
})
|
||||
.collect();
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_open_context(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::OpenContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::OpenContextResponse> {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
let operations = this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("only the host contexts can be opened"));
|
||||
}
|
||||
|
||||
let context = this
|
||||
.loaded_context_for_id(&context_id, cx)
|
||||
.context("context not found")?;
|
||||
if context.read(cx).replica_id() != ReplicaId::default() {
|
||||
return Err(anyhow!("context must be opened via the host"));
|
||||
}
|
||||
|
||||
anyhow::Ok(
|
||||
context
|
||||
.read(cx)
|
||||
.serialize_ops(&ContextVersion::default(), cx),
|
||||
)
|
||||
})??;
|
||||
let operations = operations.await;
|
||||
Ok(proto::OpenContextResponse {
|
||||
context: Some(proto::Context { operations }),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_update_context(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateContext>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let context_id = ContextId::from_proto(envelope.payload.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
let operation_proto = envelope.payload.operation.context("invalid operation")?;
|
||||
let operation = ContextOperation::from_proto(operation_proto)?;
|
||||
context.update(cx, |context, cx| context.apply_ops([operation], cx))?;
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
|
||||
async fn handle_synchronize_contexts(
|
||||
this: Model<Self>,
|
||||
envelope: TypedEnvelope<proto::SynchronizeContexts>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::SynchronizeContextsResponse> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.project.read(cx).is_remote() {
|
||||
return Err(anyhow!("only the host can synchronize contexts"));
|
||||
}
|
||||
|
||||
let mut local_versions = Vec::new();
|
||||
for remote_version_proto in envelope.payload.contexts {
|
||||
let remote_version = ContextVersion::from_proto(&remote_version_proto);
|
||||
let context_id = ContextId::from_proto(remote_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
let context = context.read(cx);
|
||||
let operations = context.serialize_ops(&remote_version, cx);
|
||||
local_versions.push(context.version(cx).to_proto(context_id.clone()));
|
||||
let client = this.client.clone();
|
||||
let project_id = envelope.payload.project_id;
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let operations = operations.await;
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
this.advertise_contexts(cx);
|
||||
|
||||
anyhow::Ok(proto::SynchronizeContextsResponse {
|
||||
contexts: local_versions,
|
||||
})
|
||||
})?
|
||||
}
|
||||
|
||||
fn handle_project_changed(&mut self, _: Model<Project>, cx: &mut ModelContext<Self>) {
|
||||
let is_shared = self.project.read(cx).is_shared();
|
||||
let was_shared = mem::replace(&mut self.project_is_shared, is_shared);
|
||||
if is_shared == was_shared {
|
||||
return;
|
||||
}
|
||||
|
||||
if is_shared {
|
||||
self.contexts.retain_mut(|context| {
|
||||
if let Some(strong_context) = context.upgrade() {
|
||||
*context = ContextHandle::Strong(strong_context);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
let remote_id = self.project.read(cx).remote_id().unwrap();
|
||||
self.client_subscription = self
|
||||
.client
|
||||
.subscribe_to_entity(remote_id)
|
||||
.log_err()
|
||||
.map(|subscription| subscription.set_model(&cx.handle(), &mut cx.to_async()));
|
||||
self.advertise_contexts(cx);
|
||||
} else {
|
||||
self.client_subscription = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_project_event(
|
||||
&mut self,
|
||||
_: Model<Project>,
|
||||
event: &project::Event,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
project::Event::Reshared => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
project::Event::HostReshared | project::Event::Rejoined => {
|
||||
self.synchronize_contexts(cx);
|
||||
}
|
||||
project::Event::DisconnectedFromHost => {
|
||||
self.contexts.retain_mut(|context| {
|
||||
if let Some(strong_context) = context.upgrade() {
|
||||
*context = ContextHandle::Weak(context.downgrade());
|
||||
strong_context.update(cx, |context, cx| {
|
||||
if context.replica_id() != ReplicaId::default() {
|
||||
context.set_capability(language::Capability::ReadOnly, cx);
|
||||
}
|
||||
});
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
self.host_contexts.clear();
|
||||
cx.notify();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(&mut self, cx: &mut ModelContext<Self>) -> Model<Context> {
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::local(self.languages.clone(), Some(self.telemetry.clone()), cx)
|
||||
});
|
||||
self.register_context(&context, cx);
|
||||
context
|
||||
}
|
||||
|
||||
pub fn open_local_context(
|
||||
&mut self,
|
||||
path: PathBuf,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
if let Some(existing_context) = self.loaded_context_for_path(&path, cx) {
|
||||
return Task::ready(Ok(existing_context));
|
||||
}
|
||||
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let saved_context = fs.load(&path).await?;
|
||||
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
|
||||
match saved_context_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedContext::VERSION => {
|
||||
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
|
||||
}
|
||||
"0.2.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_2_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
"0.1.0" => {
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
slash_command_output_sections: Vec::new(),
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved context")),
|
||||
let languages = self.languages.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let load = cx.background_executor().spawn({
|
||||
let path = path.clone();
|
||||
async move {
|
||||
let saved_context = fs.load(&path).await?;
|
||||
SavedContext::from_json(&saved_context)
|
||||
}
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let saved_context = load.await?;
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::deserialize(saved_context, path.clone(), languages, Some(telemetry), cx)
|
||||
})?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_path(&path, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn loaded_context_for_path(&self, path: &Path, cx: &AppContext) -> Option<Model<Context>> {
|
||||
self.contexts.iter().find_map(|context| {
|
||||
let context = context.upgrade()?;
|
||||
if context.read(cx).path() == Some(path) {
|
||||
Some(context)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn loaded_context_for_id(&self, id: &ContextId, cx: &AppContext) -> Option<Model<Context>> {
|
||||
self.contexts.iter().find_map(|context| {
|
||||
let context = context.upgrade()?;
|
||||
if context.read(cx).id() == id {
|
||||
Some(context)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_remote_context(
|
||||
&mut self,
|
||||
context_id: ContextId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Context>>> {
|
||||
let project = self.project.read(cx);
|
||||
let Some(project_id) = project.remote_id() else {
|
||||
return Task::ready(Err(anyhow!("project was not remote")));
|
||||
};
|
||||
if project.is_local() {
|
||||
return Task::ready(Err(anyhow!("cannot open remote contexts as the host")));
|
||||
}
|
||||
|
||||
if let Some(context) = self.loaded_context_for_id(&context_id, cx) {
|
||||
return Task::ready(Ok(context));
|
||||
}
|
||||
|
||||
let replica_id = project.replica_id();
|
||||
let capability = project.capability();
|
||||
let language_registry = self.languages.clone();
|
||||
let telemetry = self.telemetry.clone();
|
||||
let request = self.client.request(proto::OpenContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = request.await?;
|
||||
let context_proto = response.context.context("invalid context")?;
|
||||
let context = cx.new_model(|cx| {
|
||||
Context::new(
|
||||
context_id.clone(),
|
||||
replica_id,
|
||||
capability,
|
||||
language_registry,
|
||||
Some(telemetry),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
let operations = cx
|
||||
.background_executor()
|
||||
.spawn(async move {
|
||||
context_proto
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(|op| ContextOperation::from_proto(op))
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.await?;
|
||||
context.update(&mut cx, |context, cx| context.apply_ops(operations, cx))??;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(existing_context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
existing_context
|
||||
} else {
|
||||
this.register_context(&context, cx);
|
||||
this.synchronize_contexts(cx);
|
||||
context
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn register_context(&mut self, context: &Model<Context>, cx: &mut ModelContext<Self>) {
|
||||
let handle = if self.project_is_shared {
|
||||
ContextHandle::Strong(context.clone())
|
||||
} else {
|
||||
ContextHandle::Weak(context.downgrade())
|
||||
};
|
||||
self.contexts.push(handle);
|
||||
self.advertise_contexts(cx);
|
||||
cx.subscribe(context, Self::handle_context_event).detach();
|
||||
}
|
||||
|
||||
fn handle_context_event(
|
||||
&mut self,
|
||||
context: Model<Context>,
|
||||
event: &ContextEvent,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match event {
|
||||
ContextEvent::SummaryChanged => {
|
||||
self.advertise_contexts(cx);
|
||||
}
|
||||
ContextEvent::Operation(operation) => {
|
||||
let context_id = context.read(cx).id().to_proto();
|
||||
let operation = operation.to_proto();
|
||||
self.client
|
||||
.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id,
|
||||
operation: Some(operation),
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn advertise_contexts(&self, cx: &AppContext) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// For now, only the host can advertise their open contexts.
|
||||
if self.project.read(cx).is_remote() {
|
||||
return;
|
||||
}
|
||||
|
||||
let contexts = self
|
||||
.contexts
|
||||
.iter()
|
||||
.rev()
|
||||
.filter_map(|context| {
|
||||
let context = context.upgrade()?.read(cx);
|
||||
if context.replica_id() == ReplicaId::default() {
|
||||
Some(proto::ContextMetadata {
|
||||
context_id: context.id().to_proto(),
|
||||
summary: context.summary().map(|summary| summary.text.clone()),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
self.client
|
||||
.send(proto::AdvertiseContexts {
|
||||
project_id,
|
||||
contexts,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
fn synchronize_contexts(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let Some(project_id) = self.project.read(cx).remote_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let contexts = self
|
||||
.contexts
|
||||
.iter()
|
||||
.filter_map(|context| {
|
||||
let context = context.upgrade()?.read(cx);
|
||||
if context.replica_id() != ReplicaId::default() {
|
||||
Some(context.version(cx).to_proto(context.id().clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let client = self.client.clone();
|
||||
let request = self.client.request(proto::SynchronizeContexts {
|
||||
project_id,
|
||||
contexts,
|
||||
});
|
||||
cx.spawn(|this, cx| async move {
|
||||
let response = request.await?;
|
||||
|
||||
let mut context_ids = Vec::new();
|
||||
let mut operations = Vec::new();
|
||||
this.read_with(&cx, |this, cx| {
|
||||
for context_version_proto in response.contexts {
|
||||
let context_version = ContextVersion::from_proto(&context_version_proto);
|
||||
let context_id = ContextId::from_proto(context_version_proto.context_id);
|
||||
if let Some(context) = this.loaded_context_for_id(&context_id, cx) {
|
||||
context_ids.push(context_id);
|
||||
operations.push(context.read(cx).serialize_ops(&context_version, cx));
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
let operations = futures::future::join_all(operations).await;
|
||||
for (context_id, operations) in context_ids.into_iter().zip(operations) {
|
||||
for operation in operations {
|
||||
client.send(proto::UpdateContext {
|
||||
project_id,
|
||||
context_id: context_id.to_proto(),
|
||||
operation: Some(operation),
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
let metadata = self.contexts_metadata.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
@@ -178,6 +577,10 @@ impl ContextStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn host_contexts(&self) -> &[RemoteContextMetadata] {
|
||||
&self.host_contexts
|
||||
}
|
||||
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
||||
@@ -3,7 +3,6 @@ use crate::{
|
||||
InlineAssist, InlineAssistant, LanguageModelRequest, LanguageModelRequestMessage, Role,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
|
||||
@@ -448,7 +447,6 @@ impl PromptLibrary {
|
||||
self.set_active_prompt(Some(prompt_id), cx);
|
||||
} else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
|
||||
let language_registry = self.language_registry.clone();
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let prompt = self.store.load(prompt_id);
|
||||
self.pending_load = cx.spawn(|this, mut cx| async move {
|
||||
let prompt = prompt.await;
|
||||
@@ -477,7 +475,7 @@ impl PromptLibrary {
|
||||
editor.set_use_modal_editing(false);
|
||||
editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
|
||||
editor.set_completion_provider(Box::new(
|
||||
SlashCommandCompletionProvider::new(commands, None, None),
|
||||
SlashCommandCompletionProvider::new(None, None),
|
||||
));
|
||||
if focus {
|
||||
editor.focus(cx);
|
||||
|
||||
@@ -31,7 +31,6 @@ pub mod tabs_command;
|
||||
pub mod term_command;
|
||||
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
@@ -46,14 +45,12 @@ pub(crate) struct SlashCommandLine {
|
||||
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancel_flag: Mutex::new(Arc::new(AtomicBool::new(false))),
|
||||
editor,
|
||||
commands,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
@@ -65,8 +62,8 @@ impl SlashCommandCompletionProvider {
|
||||
name_range: Range<Anchor>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Task<Result<Vec<project::Completion>>> {
|
||||
let candidates = self
|
||||
.commands
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
let candidates = commands
|
||||
.command_names()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
@@ -76,7 +73,6 @@ impl SlashCommandCompletionProvider {
|
||||
char_bag: def.as_ref().into(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let commands = self.commands.clone();
|
||||
let command_name = command_name.to_string();
|
||||
let editor = self.editor.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
@@ -155,7 +151,8 @@ impl SlashCommandCompletionProvider {
|
||||
flag.store(true, SeqCst);
|
||||
*flag = new_cancel_flag.clone();
|
||||
|
||||
if let Some(command) = self.commands.command(command_name) {
|
||||
let commands = SlashCommandRegistry::global(cx);
|
||||
if let Some(command) = commands.command(command_name) {
|
||||
let completions = command.complete_argument(
|
||||
argument,
|
||||
new_cancel_flag.clone(),
|
||||
|
||||
@@ -67,7 +67,7 @@ pub struct SlashCommandOutput {
|
||||
pub run_commands_in_text: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SlashCommandOutputSection<T> {
|
||||
pub range: Range<T>,
|
||||
pub icon: IconName,
|
||||
|
||||
@@ -72,7 +72,7 @@ impl Render for Breadcrumbs {
|
||||
.into_any()
|
||||
});
|
||||
let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
|
||||
Label::new("›").color(Color::Muted).into_any_element()
|
||||
Label::new("›").color(Color::Placeholder).into_any_element()
|
||||
});
|
||||
|
||||
let breadcrumbs_stack = h_flex().gap_1().children(breadcrumbs);
|
||||
@@ -83,7 +83,7 @@ impl Render for Breadcrumbs {
|
||||
Some(editor) => element.child(
|
||||
ButtonLike::new("toggle outline view")
|
||||
.child(breadcrumbs_stack)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(move |_, cx| {
|
||||
if let Some(editor) = editor.upgrade() {
|
||||
outline::toggle(editor, &editor::actions::ToggleOutline, cx)
|
||||
|
||||
@@ -217,6 +217,9 @@ pub struct Client {
|
||||
>,
|
||||
>,
|
||||
>,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
rpc_url: RwLock<Option<Url>>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -527,6 +530,8 @@ impl Client {
|
||||
authenticate: Default::default(),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
establish_connection: Default::default(),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
rpc_url: RwLock::default(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -584,6 +589,12 @@ impl Client {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn override_rpc_url(&self, url: Url) -> &Self {
|
||||
*self.rpc_url.write() = Some(url);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn global(cx: &AppContext) -> Arc<Self> {
|
||||
cx.global::<GlobalClient>().0.clone()
|
||||
}
|
||||
@@ -1086,38 +1097,50 @@ impl Client {
|
||||
self.establish_websocket_connection(credentials, cx)
|
||||
}
|
||||
|
||||
async fn get_rpc_url(
|
||||
fn rpc_url(
|
||||
&self,
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
release_channel: Option<ReleaseChannel>,
|
||||
) -> Result<Url> {
|
||||
if let Some(url) = &*ZED_RPC_URL {
|
||||
return Url::parse(url).context("invalid rpc url");
|
||||
}
|
||||
) -> impl Future<Output = Result<Url>> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let url_override = self.rpc_url.read().clone();
|
||||
|
||||
let mut url = http.build_url("/rpc");
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
let collab_url = if response.status().is_redirection() {
|
||||
response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string()
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"unexpected /rpc response status {}",
|
||||
response.status()
|
||||
))?
|
||||
};
|
||||
async move {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if let Some(url) = url_override {
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
Url::parse(&collab_url).context("invalid rpc url")
|
||||
if let Some(url) = &*ZED_RPC_URL {
|
||||
return Url::parse(url).context("invalid rpc url");
|
||||
}
|
||||
|
||||
let mut url = http.build_url("/rpc");
|
||||
if let Some(preview_param) =
|
||||
release_channel.and_then(|channel| channel.release_query_param())
|
||||
{
|
||||
url += "?";
|
||||
url += preview_param;
|
||||
}
|
||||
|
||||
let response = http.get(&url, Default::default(), false).await?;
|
||||
let collab_url = if response.status().is_redirection() {
|
||||
response
|
||||
.headers()
|
||||
.get("Location")
|
||||
.ok_or_else(|| anyhow!("missing location header in /rpc response"))?
|
||||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string()
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"unexpected /rpc response status {}",
|
||||
response.status()
|
||||
))?
|
||||
};
|
||||
|
||||
Url::parse(&collab_url).context("invalid rpc url")
|
||||
}
|
||||
}
|
||||
|
||||
fn establish_websocket_connection(
|
||||
@@ -1144,8 +1167,9 @@ impl Client {
|
||||
);
|
||||
|
||||
let http = self.http.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
cx.background_executor().spawn(async move {
|
||||
let mut rpc_url = Self::get_rpc_url(http, release_channel).await?;
|
||||
let mut rpc_url = rpc_url.await?;
|
||||
let rpc_host = rpc_url
|
||||
.host_str()
|
||||
.zip(rpc_url.port_or_known_default())
|
||||
@@ -1186,6 +1210,7 @@ impl Client {
|
||||
cx: &AsyncAppContext,
|
||||
) -> Task<Result<Credentials>> {
|
||||
let http = self.http.clone();
|
||||
let this = self.clone();
|
||||
cx.spawn(|cx| async move {
|
||||
let background = cx.background_executor().clone();
|
||||
|
||||
@@ -1215,7 +1240,8 @@ impl Client {
|
||||
{
|
||||
eprintln!("authenticate as admin {login}, {token}");
|
||||
|
||||
return Self::authenticate_as_admin(http, login.clone(), token.clone())
|
||||
return this
|
||||
.authenticate_as_admin(http, login.clone(), token.clone())
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -1303,6 +1329,7 @@ impl Client {
|
||||
}
|
||||
|
||||
async fn authenticate_as_admin(
|
||||
self: &Arc<Self>,
|
||||
http: Arc<HttpClientWithUrl>,
|
||||
login: String,
|
||||
mut api_token: String,
|
||||
@@ -1319,7 +1346,7 @@ impl Client {
|
||||
|
||||
// Use the collab server's admin API to retrieve the id
|
||||
// of the impersonated user.
|
||||
let mut url = Self::get_rpc_url(http.clone(), None).await?;
|
||||
let mut url = self.rpc_url(http.clone(), None).await?;
|
||||
url.set_path("/user");
|
||||
url.set_query(Some(&format!("github_login={login}")));
|
||||
let request = Request::get(url.as_str())
|
||||
|
||||
@@ -18,4 +18,5 @@ test-support = ["dep:parking_lot"]
|
||||
[dependencies]
|
||||
chrono.workspace = true
|
||||
parking_lot = { workspace = true, optional = true }
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod system_clock;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
@@ -16,7 +17,7 @@ pub type Seq = u32;
|
||||
|
||||
/// A [Lamport timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp),
|
||||
/// used to determine the ordering of events in the editor.
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq)]
|
||||
#[derive(Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Lamport {
|
||||
pub replica_id: ReplicaId,
|
||||
pub value: Seq,
|
||||
@@ -161,6 +162,10 @@ impl Lamport {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
((self.value as u64) << 32) | (self.replica_id as u64)
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) -> Self {
|
||||
let timestamp = *self;
|
||||
self.value += 1;
|
||||
|
||||
@@ -71,6 +71,7 @@ util.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant = { workspace = true, features = ["test-support"] }
|
||||
async-trait.workspace = true
|
||||
audio.workspace = true
|
||||
call = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -562,7 +562,7 @@ fn test_fuzzy_like_string() {
|
||||
assert_eq!(Database::fuzzy_like_string(" z "), "%z%");
|
||||
}
|
||||
|
||||
#[cfg(target = "macos")]
|
||||
#[cfg(target_os = "macos")]
|
||||
#[gpui::test]
|
||||
async fn test_fuzzy_search_users(cx: &mut gpui::TestAppContext) {
|
||||
let test_db = tests::TestDb::postgres(cx.executor());
|
||||
|
||||
@@ -595,6 +595,14 @@ impl Server {
|
||||
.add_message_handler(user_message_handler(acknowledge_channel_message))
|
||||
.add_message_handler(user_message_handler(acknowledge_buffer_version))
|
||||
.add_request_handler(user_handler(get_supermaven_api_key))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::OpenContext>,
|
||||
))
|
||||
.add_request_handler(user_handler(
|
||||
forward_mutating_project_request::<proto::SynchronizeContexts>,
|
||||
))
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
.add_streaming_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
move |request, response, session| {
|
||||
@@ -3056,6 +3064,53 @@ async fn update_buffer(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_context(message: proto::UpdateContext, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(message.project_id);
|
||||
|
||||
let operation = message.operation.as_ref().context("invalid operation")?;
|
||||
let capability = match operation.variant.as_ref() {
|
||||
Some(proto::context_operation::Variant::BufferOperation(buffer_op)) => {
|
||||
if let Some(buffer_op) = buffer_op.operation.as_ref() {
|
||||
match buffer_op.variant {
|
||||
None | Some(proto::operation::Variant::UpdateSelections(_)) => {
|
||||
Capability::ReadOnly
|
||||
}
|
||||
_ => Capability::ReadWrite,
|
||||
}
|
||||
} else {
|
||||
Capability::ReadWrite
|
||||
}
|
||||
}
|
||||
Some(_) => Capability::ReadWrite,
|
||||
None => Capability::ReadOnly,
|
||||
};
|
||||
|
||||
let guard = session
|
||||
.db()
|
||||
.await
|
||||
.connections_for_buffer_update(
|
||||
project_id,
|
||||
session.principal_id(),
|
||||
session.connection_id,
|
||||
capability,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (host, guests) = &*guard;
|
||||
|
||||
broadcast(
|
||||
Some(session.connection_id),
|
||||
guests.iter().chain([host]).copied(),
|
||||
|connection_id| {
|
||||
session
|
||||
.peer
|
||||
.forward_send(session.connection_id, connection_id, message.clone())
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Notify other participants that a project has been updated.
|
||||
async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
|
||||
request: T,
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant::ContextStore;
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{User, RECEIVE_TIMEOUT};
|
||||
use collections::{HashMap, HashSet};
|
||||
@@ -6449,3 +6450,123 @@ async fn test_preview_tabs(cx: &mut TestAppContext) {
|
||||
assert!(!pane.can_navigate_forward());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_context_collaboration_with_reconnect(
|
||||
executor: BackgroundExecutor,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
let mut server = TestServer::start(executor.clone()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a.fs().insert_tree("/a", Default::default()).await;
|
||||
let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_b = client_b.build_dev_server_project(project_id, cx_b).await;
|
||||
|
||||
// Client A sees that a guest has joined.
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.read_with(cx_a, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
project_b.read_with(cx_b, |project, _| {
|
||||
assert_eq!(project.collaborators().len(), 1);
|
||||
});
|
||||
|
||||
let context_store_a = cx_a
|
||||
.update(|cx| ContextStore::new(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let context_store_b = cx_b
|
||||
.update(|cx| ContextStore::new(project_b.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Client A creates a new context.
|
||||
let context_a = context_store_a.update(cx_a, |store, cx| store.create(cx));
|
||||
executor.run_until_parked();
|
||||
|
||||
// Client B retrieves host's contexts and joins one.
|
||||
let context_b = context_store_b
|
||||
.update(cx_b, |store, cx| {
|
||||
let host_contexts = store.host_contexts().to_vec();
|
||||
assert_eq!(host_contexts.len(), 1);
|
||||
store.open_remote_context(host_contexts[0].id.clone(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Host and guest make changes
|
||||
context_a.update(cx_a, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Host change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
context_b.update(cx_b, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Guest change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Disconnect client A and make some changes while disconnected.
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
server.forbid_connections();
|
||||
context_a.update(cx_a, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Host offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
context_b.update(cx_b, |context, cx| {
|
||||
context.buffer().update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "Guest offline change\n")], None, cx)
|
||||
})
|
||||
});
|
||||
executor.run_until_parked();
|
||||
assert_eq!(
|
||||
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
|
||||
"Host offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Allow client A to reconnect and verify that contexts converge.
|
||||
server.allow_connections();
|
||||
executor.advance_clock(RECEIVE_TIMEOUT);
|
||||
assert_eq!(
|
||||
context_a.read_with(cx_a, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
assert_eq!(
|
||||
context_b.read_with(cx_b, |context, cx| context.buffer().read(cx).text()),
|
||||
"Guest offline change\nHost offline change\nGuest change\nHost change\n"
|
||||
);
|
||||
|
||||
// Client A disconnects without being able to reconnect. Context B becomes readonly.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_a.peer_id().unwrap());
|
||||
executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT);
|
||||
context_b.read_with(cx_b, |context, cx| {
|
||||
assert!(context.buffer().read(cx).read_only());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -294,6 +294,8 @@ impl TestServer {
|
||||
menu::init();
|
||||
dev_server_projects::init(client.clone(), cx);
|
||||
settings::KeymapFile::load_asset(os_keymap, cx).unwrap();
|
||||
assistant::FakeCompletionProvider::setup_test(cx);
|
||||
assistant::context_store::init(&client);
|
||||
});
|
||||
|
||||
client
|
||||
|
||||
@@ -11093,6 +11093,7 @@ impl Editor {
|
||||
if *singleton_buffer_edited {
|
||||
if let Some(project) = &self.project {
|
||||
let project = project.read(cx);
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let languages_affected = multibuffer
|
||||
.read(cx)
|
||||
.all_buffers()
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::env;
|
||||
|
||||
fn main() {
|
||||
let target = env::var("CARGO_CFG_TARGET_OS");
|
||||
|
||||
println!("cargo::rustc-check-cfg=cfg(gles)");
|
||||
match target.as_deref() {
|
||||
Ok("macos") => {
|
||||
#[cfg(target_os = "macos")]
|
||||
|
||||
@@ -193,16 +193,6 @@ impl TextInput {
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(self.content.len())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.content = "".into();
|
||||
self.selected_range = 0..0;
|
||||
self.selection_reversed = false;
|
||||
self.marked_range = None;
|
||||
self.last_layout = None;
|
||||
self.last_bounds = None;
|
||||
self.is_selecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewInputHandler for TextInput {
|
||||
@@ -325,7 +315,6 @@ impl Element for TextElement {
|
||||
None
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
@@ -337,7 +326,6 @@ impl Element for TextElement {
|
||||
(cx.request_layout(style, []), ())
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
@@ -428,7 +416,6 @@ impl Element for TextElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
@@ -443,14 +430,12 @@ impl Element for TextElement {
|
||||
ElementInputHandler::new(bounds, self.input.clone()),
|
||||
);
|
||||
if let Some(selection) = prepaint.selection.take() {
|
||||
profiling::scope!("paint_quad selection");
|
||||
cx.paint_quad(selection)
|
||||
}
|
||||
let line = prepaint.line.take().unwrap();
|
||||
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
|
||||
|
||||
if let Some(cursor) = prepaint.cursor.take() {
|
||||
profiling::scope!("paint_quad cursor");
|
||||
cx.paint_quad(cursor);
|
||||
}
|
||||
self.input.update(cx, |input, _cx| {
|
||||
@@ -461,7 +446,6 @@ impl Element for TextElement {
|
||||
}
|
||||
|
||||
impl Render for TextInput {
|
||||
#[profiling::function]
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
@@ -510,48 +494,13 @@ struct InputExample {
|
||||
recent_keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl InputExample {
|
||||
fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
|
||||
self.recent_keystrokes.clear();
|
||||
self.text_input
|
||||
.update(cx, |text_input, _cx| text_input.reset());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InputExample {
|
||||
#[profiling::function]
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let num_keystrokes = self.recent_keystrokes.len();
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(rgb(0xaaaaaa))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.bg(white())
|
||||
.border_b_1()
|
||||
.border_color(black())
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(format!("Keystrokes: {}", num_keystrokes))
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(black())
|
||||
.px_2()
|
||||
.bg(yellow())
|
||||
.child("Reset")
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(yellow().blend(opaque_grey(0.5, 0.5)))
|
||||
.cursor_pointer()
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
|
||||
),
|
||||
)
|
||||
.child(self.text_input.clone())
|
||||
.children(self.recent_keystrokes.iter().rev().map(|ks| {
|
||||
format!(
|
||||
|
||||
@@ -2,63 +2,209 @@ use gpui::*;
|
||||
|
||||
struct WindowContent {
|
||||
text: SharedString,
|
||||
bounds: Bounds<Pixels>,
|
||||
bg: Hsla,
|
||||
}
|
||||
|
||||
impl Render for WindowContent {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let window_bounds = cx.bounds();
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.bg(rgb(0x1e2025))
|
||||
.flex_col()
|
||||
.bg(self.bg)
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(self.text.clone())
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.text_sm()
|
||||
.items_center()
|
||||
.size_full()
|
||||
.child(format!(
|
||||
"origin: {}, {} size: {}, {}",
|
||||
self.bounds.origin.x,
|
||||
self.bounds.origin.y,
|
||||
self.bounds.size.width,
|
||||
self.bounds.size.height
|
||||
))
|
||||
.child(format!(
|
||||
"cx.bounds() origin: {}, {} size {}, {}",
|
||||
window_bounds.origin.x,
|
||||
window_bounds.origin.y,
|
||||
window_bounds.size.width,
|
||||
window_bounds.size.height
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_window_options(display_id: DisplayId, bounds: Bounds<Pixels>) -> WindowOptions {
|
||||
WindowOptions {
|
||||
// Set the bounds of the window in screen coordinates
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
// Specify the display_id to ensure the window is created on the correct screen
|
||||
display_id: Some(display_id),
|
||||
titlebar: None,
|
||||
window_background: WindowBackgroundAppearance::Transparent,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
// Create several new windows, positioned in the top right corner of each screen
|
||||
let size = Size {
|
||||
width: px(350.),
|
||||
height: px(75.),
|
||||
};
|
||||
let margin_offset = px(150.);
|
||||
|
||||
for screen in cx.displays() {
|
||||
let options = {
|
||||
let margin_right = px(16.);
|
||||
let margin_height = px(-48.);
|
||||
|
||||
let size = Size {
|
||||
width: px(400.),
|
||||
height: px(72.),
|
||||
};
|
||||
|
||||
let bounds = gpui::Bounds::<Pixels> {
|
||||
origin: screen.bounds().upper_right()
|
||||
- point(size.width + margin_right, margin_height),
|
||||
size,
|
||||
};
|
||||
|
||||
WindowOptions {
|
||||
// Set the bounds of the window in screen coordinates
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
// Specify the display_id to ensure the window is created on the correct screen
|
||||
display_id: Some(screen.id()),
|
||||
|
||||
titlebar: None,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: None,
|
||||
window_decorations: None,
|
||||
}
|
||||
let bounds = Bounds {
|
||||
origin: point(margin_offset, margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(options, |cx| {
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("{:?}", screen.id()).into(),
|
||||
text: format!("Top Left {:?}", screen.id()).into(),
|
||||
bg: gpui::red(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().upper_right()
|
||||
- point(size.width + margin_offset, -margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Top Right {:?}", screen.id()).into(),
|
||||
bg: gpui::red(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().lower_left()
|
||||
- point(-margin_offset, size.height + margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Bottom Left {:?}", screen.id()).into(),
|
||||
bg: gpui::blue(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: screen.bounds().lower_right()
|
||||
- point(size.width + margin_offset, size.height + margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Bottom Right {:?}", screen.id()).into(),
|
||||
bg: gpui::blue(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(screen.bounds().center().x - size.center().x, margin_offset),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Top Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(margin_offset, screen.bounds().center().y - size.center().y),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Left Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().center().x - size.center().x,
|
||||
screen.bounds().center().y - size.center().y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().size.width - size.width - margin_offset,
|
||||
screen.bounds().center().y - size.center().y,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Right Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let bounds = Bounds {
|
||||
origin: point(
|
||||
screen.bounds().center().x - size.center().x,
|
||||
screen.bounds().size.height - size.height - margin_offset,
|
||||
),
|
||||
size,
|
||||
};
|
||||
|
||||
cx.open_window(build_window_options(screen.id(), bounds), |cx| {
|
||||
cx.new_view(|_| WindowContent {
|
||||
text: format!("Bottom Center {:?}", screen.id()).into(),
|
||||
bg: gpui::black(),
|
||||
bounds,
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
@@ -387,7 +387,6 @@ impl<E: Element> Drawable<E> {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub(crate) fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
@@ -504,7 +503,6 @@ impl AnyElement {
|
||||
}
|
||||
|
||||
/// Performs layout for this element within the given available space and returns its size.
|
||||
#[profiling::function]
|
||||
pub fn layout_as_root(
|
||||
&mut self,
|
||||
available_space: Size<AvailableSpace>,
|
||||
@@ -519,7 +517,6 @@ impl AnyElement {
|
||||
}
|
||||
|
||||
/// Performs layout on this element in the available space, then prepaints it at the given absolute origin.
|
||||
#[profiling::function]
|
||||
pub fn prepaint_as_root(
|
||||
&mut self,
|
||||
origin: Point<Pixels>,
|
||||
@@ -527,10 +524,7 @@ impl AnyElement {
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
self.layout_as_root(available_space, cx);
|
||||
cx.with_absolute_element_offset(origin, |cx| {
|
||||
profiling::scope!("with_absolute_element_offset prepaint");
|
||||
self.0.prepaint(cx)
|
||||
});
|
||||
cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -318,6 +318,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
) -> Option<oneshot::Receiver<usize>>;
|
||||
fn activate(&self);
|
||||
fn is_active(&self) -> bool;
|
||||
fn is_hovered(&self) -> bool;
|
||||
fn set_title(&mut self, title: &str);
|
||||
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance);
|
||||
fn minimize(&self);
|
||||
@@ -327,6 +328,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn on_request_frame(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
|
||||
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>);
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
|
||||
fn on_moved(&self, callback: Box<dyn FnMut()>);
|
||||
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
|
||||
|
||||
@@ -524,7 +524,6 @@ impl BladeRenderer {
|
||||
self.gpu.destroy_command_encoder(&mut self.command_encoder);
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn draw(&mut self, scene: &Scene) {
|
||||
self.command_encoder.start();
|
||||
self.atlas.before_frame(&mut self.command_encoder);
|
||||
|
||||
@@ -38,8 +38,6 @@ impl LinuxDispatcher {
|
||||
.map(|i| {
|
||||
let receiver = background_receiver.clone();
|
||||
std::thread::spawn(move || {
|
||||
let thread_name = format!("background-{}", i);
|
||||
profiling::register_thread!(&thread_name);
|
||||
for runnable in receiver {
|
||||
let start = Instant::now();
|
||||
|
||||
@@ -57,8 +55,6 @@ impl LinuxDispatcher {
|
||||
|
||||
let (timer_sender, timer_channel) = calloop::channel::channel::<TimerAfter>();
|
||||
let timer_thread = std::thread::spawn(|| {
|
||||
profiling::register_thread!("timer-thread");
|
||||
|
||||
let mut event_loop: EventLoop<()> =
|
||||
EventLoop::try_new().expect("Failed to initialize timer loop!");
|
||||
|
||||
|
||||
@@ -1403,6 +1403,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
|
||||
if let Some(window) = get_window(&mut state, &surface.id()) {
|
||||
state.mouse_focused_window = Some(window.clone());
|
||||
|
||||
if state.enter_token.is_some() {
|
||||
state.enter_token = None;
|
||||
}
|
||||
@@ -1416,7 +1417,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
drop(state);
|
||||
window.set_focused(true);
|
||||
window.set_hovered(true);
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Leave { .. } => {
|
||||
@@ -1432,7 +1433,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
|
||||
drop(state);
|
||||
focused_window.handle_input(input);
|
||||
focused_window.set_focused(false);
|
||||
focused_window.set_hovered(false);
|
||||
}
|
||||
}
|
||||
wl_pointer::Event::Motion {
|
||||
|
||||
@@ -36,6 +36,7 @@ pub(crate) struct Callbacks {
|
||||
request_frame: Option<Box<dyn FnMut()>>,
|
||||
input: Option<Box<dyn FnMut(crate::PlatformInput) -> crate::DispatchEventResult>>,
|
||||
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
hover_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved: Option<Box<dyn FnMut()>>,
|
||||
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
@@ -97,6 +98,7 @@ pub struct WaylandWindowState {
|
||||
client: WaylandClientStatePtr,
|
||||
handle: AnyWindowHandle,
|
||||
active: bool,
|
||||
hovered: bool,
|
||||
in_progress_configure: Option<InProgressConfigure>,
|
||||
in_progress_window_controls: Option<WindowControls>,
|
||||
window_controls: WindowControls,
|
||||
@@ -181,6 +183,7 @@ impl WaylandWindowState {
|
||||
appearance,
|
||||
handle,
|
||||
active: false,
|
||||
hovered: false,
|
||||
in_progress_window_controls: None,
|
||||
// Assume that we can do anything, unless told otherwise
|
||||
window_controls: WindowControls {
|
||||
@@ -700,6 +703,12 @@ impl WaylandWindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_hovered(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().hover_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
|
||||
self.state.borrow_mut().appearance = appearance;
|
||||
|
||||
@@ -845,6 +854,10 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.borrow().active
|
||||
}
|
||||
|
||||
fn is_hovered(&self) -> bool {
|
||||
self.borrow().hovered
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.borrow().toplevel.set_title(title.to_string());
|
||||
}
|
||||
@@ -899,6 +912,10 @@ impl PlatformWindow for WaylandWindow {
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().hover_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
|
||||
@@ -110,7 +110,8 @@ pub struct X11ClientState {
|
||||
pub(crate) _resource_database: Database,
|
||||
pub(crate) atoms: XcbAtoms,
|
||||
pub(crate) windows: HashMap<xproto::Window, WindowRef>,
|
||||
pub(crate) focused_window: Option<xproto::Window>,
|
||||
pub(crate) mouse_focused_window: Option<xproto::Window>,
|
||||
pub(crate) keyboard_focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
@@ -144,7 +145,12 @@ impl X11ClientStatePtr {
|
||||
if let Some(window_ref) = state.windows.remove(&x_window) {
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
}
|
||||
|
||||
if state.mouse_focused_window == Some(x_window) {
|
||||
state.mouse_focused_window = None;
|
||||
}
|
||||
if state.keyboard_focused_window == Some(x_window) {
|
||||
state.keyboard_focused_window = None;
|
||||
}
|
||||
state.cursor_styles.remove(&x_window);
|
||||
|
||||
if state.windows.is_empty() {
|
||||
@@ -341,7 +347,8 @@ impl X11Client {
|
||||
_resource_database: resource_database,
|
||||
atoms,
|
||||
windows: HashMap::default(),
|
||||
focused_window: None,
|
||||
mouse_focused_window: None,
|
||||
keyboard_focused_window: None,
|
||||
xkb: xkb_state,
|
||||
ximc,
|
||||
xim_handler,
|
||||
@@ -502,7 +509,7 @@ impl X11Client {
|
||||
.push(AttributeName::ClientWindow, xim_handler.window)
|
||||
.push(AttributeName::FocusWindow, xim_handler.window);
|
||||
|
||||
let window_id = state.focused_window;
|
||||
let window_id = state.keyboard_focused_window;
|
||||
drop(state);
|
||||
if let Some(window_id) = window_id {
|
||||
let window = self.get_window(window_id).unwrap();
|
||||
@@ -586,17 +593,17 @@ impl X11Client {
|
||||
}
|
||||
Event::FocusIn(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
window.set_focused(true);
|
||||
window.set_active(true);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.focused_window = Some(event.event);
|
||||
state.keyboard_focused_window = Some(event.event);
|
||||
drop(state);
|
||||
self.enable_ime();
|
||||
}
|
||||
Event::FocusOut(event) => {
|
||||
let window = self.get_window(event.event)?;
|
||||
window.set_focused(false);
|
||||
window.set_active(false);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.focused_window = None;
|
||||
state.keyboard_focused_window = None;
|
||||
if let Some(compose_state) = state.compose_state.as_mut() {
|
||||
compose_state.reset();
|
||||
}
|
||||
@@ -620,7 +627,7 @@ impl X11Client {
|
||||
if state.modifiers == modifiers {
|
||||
drop(state);
|
||||
} else {
|
||||
let focused_window_id = state.focused_window?;
|
||||
let focused_window_id = state.keyboard_focused_window?;
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
@@ -871,12 +878,18 @@ impl X11Client {
|
||||
valuator_idx += 1;
|
||||
}
|
||||
}
|
||||
Event::XinputEnter(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
let window = self.get_window(event.event)?;
|
||||
window.set_hovered(true);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.mouse_focused_window = Some(event.event);
|
||||
}
|
||||
Event::XinputLeave(event) if event.mode == xinput::NotifyMode::NORMAL => {
|
||||
self.0.borrow_mut().scroll_x = None; // Set last scroll to `None` so that a large delta isn't created if scrolling is done outside the window (the valuator is global)
|
||||
self.0.borrow_mut().scroll_y = None;
|
||||
|
||||
let window = self.get_window(event.event)?;
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.mouse_focused_window = None;
|
||||
let pressed_button = pressed_button_from_mask(event.buttons[0]);
|
||||
let position = point(
|
||||
px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor),
|
||||
@@ -886,11 +899,13 @@ impl X11Client {
|
||||
state.modifiers = modifiers;
|
||||
drop(state);
|
||||
|
||||
let window = self.get_window(event.event)?;
|
||||
window.handle_input(PlatformInput::MouseExited(crate::MouseExitEvent {
|
||||
pressed_button,
|
||||
position,
|
||||
modifiers,
|
||||
}));
|
||||
window.set_hovered(false);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -1140,7 +1155,7 @@ impl LinuxClient for X11Client {
|
||||
|
||||
fn set_cursor_style(&self, style: CursorStyle) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
let Some(focused_window) = state.focused_window else {
|
||||
let Some(focused_window) = state.mouse_focused_window else {
|
||||
return;
|
||||
};
|
||||
let current_style = state
|
||||
@@ -1272,7 +1287,7 @@ impl LinuxClient for X11Client {
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
let state = self.0.borrow();
|
||||
state.focused_window.and_then(|focused_window| {
|
||||
state.keyboard_focused_window.and_then(|focused_window| {
|
||||
state
|
||||
.windows
|
||||
.get(&focused_window)
|
||||
|
||||
@@ -211,6 +211,7 @@ pub struct Callbacks {
|
||||
request_frame: Option<Box<dyn FnMut()>>,
|
||||
input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
|
||||
active_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
hovered_status_change: Option<Box<dyn FnMut(bool)>>,
|
||||
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved: Option<Box<dyn FnMut()>>,
|
||||
should_close: Option<Box<dyn FnMut() -> bool>>,
|
||||
@@ -238,6 +239,7 @@ pub struct X11WindowState {
|
||||
maximized_horizontal: bool,
|
||||
hidden: bool,
|
||||
active: bool,
|
||||
hovered: bool,
|
||||
fullscreen: bool,
|
||||
client_side_decorations_supported: bool,
|
||||
decorations: WindowDecorations,
|
||||
@@ -451,6 +453,7 @@ impl X11WindowState {
|
||||
xinput::XIEventMask::MOTION
|
||||
| xinput::XIEventMask::BUTTON_PRESS
|
||||
| xinput::XIEventMask::BUTTON_RELEASE
|
||||
| xinput::XIEventMask::ENTER
|
||||
| xinput::XIEventMask::LEAVE,
|
||||
],
|
||||
}],
|
||||
@@ -507,6 +510,7 @@ impl X11WindowState {
|
||||
atoms: *atoms,
|
||||
input_handler: None,
|
||||
active: false,
|
||||
hovered: false,
|
||||
fullscreen: false,
|
||||
maximized_vertical: false,
|
||||
maximized_horizontal: false,
|
||||
@@ -777,6 +781,15 @@ impl X11WindowStatePtr {
|
||||
state.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
let hovered_window = self
|
||||
.xcb_connection
|
||||
.query_pointer(state.x_root_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.unwrap()
|
||||
.child;
|
||||
self.set_hovered(hovered_window == self.x_window);
|
||||
}
|
||||
|
||||
pub fn close(&self) {
|
||||
@@ -912,12 +925,18 @@ impl X11WindowStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_focused(&self, focus: bool) {
|
||||
pub fn set_active(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().active_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_hovered(&self, focus: bool) {
|
||||
if let Some(ref mut fun) = self.callbacks.borrow_mut().hovered_status_change {
|
||||
fun(focus);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_appearance(&mut self, appearance: WindowAppearance) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.appearance = appearance;
|
||||
@@ -1046,6 +1065,10 @@ impl PlatformWindow for X11Window {
|
||||
self.0.state.borrow().active
|
||||
}
|
||||
|
||||
fn is_hovered(&self) -> bool {
|
||||
self.0.state.borrow().hovered
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.0
|
||||
.xcb_connection
|
||||
@@ -1162,6 +1185,10 @@ impl PlatformWindow for X11Window {
|
||||
self.0.callbacks.borrow_mut().active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.callbacks.borrow_mut().hovered_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.callbacks.borrow_mut().resize = Some(callback);
|
||||
}
|
||||
@@ -1182,7 +1209,6 @@ impl PlatformWindow for X11Window {
|
||||
self.0.callbacks.borrow_mut().appearance_changed = Some(callback);
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn draw(&self, scene: &Scene) {
|
||||
let mut inner = self.0.state.borrow_mut();
|
||||
inner.renderer.draw(scene);
|
||||
|
||||
@@ -452,7 +452,7 @@ impl MacWindowState {
|
||||
let bounds = Bounds::new(
|
||||
point(
|
||||
px((window_frame.origin.x - screen_frame.origin.x) as f32),
|
||||
px((window_frame.origin.y - screen_frame.origin.y) as f32),
|
||||
px((window_frame.origin.y + screen_frame.origin.y) as f32),
|
||||
),
|
||||
size(
|
||||
px(window_frame.size.width as f32),
|
||||
@@ -546,7 +546,7 @@ impl MacWindow {
|
||||
let count: u64 = cocoa::foundation::NSArray::count(screens);
|
||||
for i in 0..count {
|
||||
let screen = cocoa::foundation::NSArray::objectAtIndex(screens, i);
|
||||
let frame = NSScreen::visibleFrame(screen);
|
||||
let frame = NSScreen::frame(screen);
|
||||
let display_id = display_id_for_screen(screen);
|
||||
if display_id == display.0 {
|
||||
screen_frame = Some(frame);
|
||||
@@ -557,7 +557,7 @@ impl MacWindow {
|
||||
let screen_frame = screen_frame.unwrap_or_else(|| {
|
||||
let screen = NSScreen::mainScreen(nil);
|
||||
target_screen = screen;
|
||||
NSScreen::visibleFrame(screen)
|
||||
NSScreen::frame(screen)
|
||||
});
|
||||
|
||||
let window_rect = NSRect::new(
|
||||
@@ -940,6 +940,11 @@ impl PlatformWindow for MacWindow {
|
||||
unsafe { self.0.lock().native_window.isKeyWindow() == YES }
|
||||
}
|
||||
|
||||
// is_hovered is unused on macOS. See WindowContext::is_window_hovered.
|
||||
fn is_hovered(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
unsafe {
|
||||
let app = NSApplication::sharedApplication(nil);
|
||||
@@ -1061,6 +1066,8 @@ impl PlatformWindow for MacWindow {
|
||||
self.0.as_ref().lock().activate_callback = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, _: Box<dyn FnMut(bool)>) {}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.as_ref().lock().resize_callback = Some(callback);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ pub(crate) struct TestWindowState {
|
||||
pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
|
||||
input_callback: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
|
||||
active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
hover_status_change_callback: Option<Box<dyn FnMut(bool)>>,
|
||||
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
|
||||
moved_callback: Option<Box<dyn FnMut()>>,
|
||||
input_handler: Option<PlatformInputHandler>,
|
||||
@@ -66,6 +67,7 @@ impl TestWindow {
|
||||
should_close_handler: None,
|
||||
input_callback: None,
|
||||
active_status_change_callback: None,
|
||||
hover_status_change_callback: None,
|
||||
resize_callback: None,
|
||||
moved_callback: None,
|
||||
input_handler: None,
|
||||
@@ -182,6 +184,10 @@ impl PlatformWindow for TestWindow {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_hovered(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.0.lock().title = Some(title.to_owned());
|
||||
}
|
||||
@@ -225,6 +231,10 @@ impl PlatformWindow for TestWindow {
|
||||
self.0.lock().active_status_change_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
|
||||
self.0.lock().hover_status_change_callback = Some(callback)
|
||||
}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.lock().resize_callback = Some(callback)
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@ struct DirectWriteComponent {
|
||||
|
||||
struct GlyphRenderContext {
|
||||
params: IDWriteRenderingParams3,
|
||||
normal_dc_target: ID2D1DeviceContext4,
|
||||
emoji_dc_target: ID2D1DeviceContext4,
|
||||
dc_target: ID2D1DeviceContext4,
|
||||
}
|
||||
|
||||
// All use of the IUnknown methods should be "thread-safe".
|
||||
@@ -128,16 +127,7 @@ impl GlyphRenderContext {
|
||||
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
|
||||
grid_fit_mode,
|
||||
)?;
|
||||
let normal_dc_target = {
|
||||
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
|
||||
DXGI_FORMAT_A8_UNORM,
|
||||
D2D1_ALPHA_MODE_STRAIGHT,
|
||||
))?;
|
||||
let target = target.cast::<ID2D1DeviceContext4>()?;
|
||||
target.SetTextRenderingParams(¶ms);
|
||||
target
|
||||
};
|
||||
let emoji_dc_target = {
|
||||
let dc_target = {
|
||||
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
|
||||
DXGI_FORMAT_B8G8R8A8_UNORM,
|
||||
D2D1_ALPHA_MODE_PREMULTIPLIED,
|
||||
@@ -147,11 +137,7 @@ impl GlyphRenderContext {
|
||||
target
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
params,
|
||||
normal_dc_target,
|
||||
emoji_dc_target,
|
||||
})
|
||||
Ok(Self { params, dc_target })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -571,11 +557,7 @@ impl DirectWriteState {
|
||||
}
|
||||
|
||||
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let render_target = if params.is_emoji {
|
||||
&self.components.render_context.emoji_dc_target
|
||||
} else {
|
||||
&self.components.render_context.normal_dc_target
|
||||
};
|
||||
let render_target = &self.components.render_context.dc_target;
|
||||
unsafe {
|
||||
render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS);
|
||||
render_target.SetDpi(96.0 * params.scale_factor, 96.0 * params.scale_factor);
|
||||
|
||||
@@ -503,6 +503,11 @@ impl PlatformWindow for WindowsWindow {
|
||||
self.0.hwnd == unsafe { GetActiveWindow() }
|
||||
}
|
||||
|
||||
// is_hovered is unused on Windows. See WindowContext::is_window_hovered.
|
||||
fn is_hovered(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) }
|
||||
.inspect_err(|e| log::error!("Set title failed: {e}"))
|
||||
@@ -604,6 +609,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
self.0.state.borrow_mut().callbacks.active_status_change = Some(callback);
|
||||
}
|
||||
|
||||
fn on_hover_status_change(&self, _: Box<dyn FnMut(bool)>) {}
|
||||
|
||||
fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
|
||||
self.0.state.borrow_mut().callbacks.resize = Some(callback);
|
||||
}
|
||||
|
||||
@@ -142,7 +142,6 @@ impl TaffyLayoutEngine {
|
||||
Ok(edges)
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub fn compute_layout(
|
||||
&mut self,
|
||||
id: LayoutId,
|
||||
@@ -162,7 +161,6 @@ impl TaffyLayoutEngine {
|
||||
//
|
||||
|
||||
if !self.computed_layouts.insert(id) {
|
||||
profiling::scope!("compute layout stack extension");
|
||||
let mut stack = SmallVec::<[LayoutId; 64]>::new();
|
||||
stack.push(id);
|
||||
while let Some(id) = stack.pop() {
|
||||
@@ -183,8 +181,6 @@ impl TaffyLayoutEngine {
|
||||
id.into(),
|
||||
available_space.into(),
|
||||
|known_dimensions, available_space, node_id, _context| {
|
||||
profiling::scope!("measure function");
|
||||
|
||||
let Some(measure) = self.nodes_to_measure.get_mut(&node_id.into()) else {
|
||||
return taffy::geometry::Size::default();
|
||||
};
|
||||
@@ -194,10 +190,7 @@ impl TaffyLayoutEngine {
|
||||
height: known_dimensions.height.map(Pixels),
|
||||
};
|
||||
|
||||
{
|
||||
profiling::scope!("calling measure");
|
||||
measure(known_dimensions, available_space.into(), cx).into()
|
||||
}
|
||||
measure(known_dimensions, available_space.into(), cx).into()
|
||||
},
|
||||
)
|
||||
.expect(EXPECT_MESSAGE);
|
||||
|
||||
@@ -658,26 +658,6 @@ impl Hash for RenderGlyphParams {
|
||||
}
|
||||
}
|
||||
|
||||
/// The parameters for rendering an emoji glyph.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct RenderEmojiParams {
|
||||
pub(crate) font_id: FontId,
|
||||
pub(crate) glyph_id: GlyphId,
|
||||
pub(crate) font_size: Pixels,
|
||||
pub(crate) scale_factor: f32,
|
||||
}
|
||||
|
||||
impl Eq for RenderEmojiParams {}
|
||||
|
||||
impl Hash for RenderEmojiParams {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.font_id.0.hash(state);
|
||||
self.glyph_id.0.hash(state);
|
||||
self.font_size.0.to_bits().hash(state);
|
||||
self.scale_factor.to_bits().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// The configuration details for identifying a specific font.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct Font {
|
||||
|
||||
@@ -49,9 +49,17 @@ impl LineWrapper {
|
||||
continue;
|
||||
}
|
||||
|
||||
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
|
||||
last_candidate_ix = ix;
|
||||
last_candidate_width = width;
|
||||
if Self::is_word_char(c) {
|
||||
if prev_c == ' ' && c != ' ' && first_non_whitespace_ix.is_some() {
|
||||
last_candidate_ix = ix;
|
||||
last_candidate_width = width;
|
||||
}
|
||||
} else {
|
||||
// CJK may not be space separated, e.g.: `Hello world你好世界`
|
||||
if c != ' ' && first_non_whitespace_ix.is_some() {
|
||||
last_candidate_ix = ix;
|
||||
last_candidate_width = width;
|
||||
}
|
||||
}
|
||||
|
||||
if c != ' ' && first_non_whitespace_ix.is_none() {
|
||||
@@ -90,6 +98,31 @@ impl LineWrapper {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn is_word_char(c: char) -> bool {
|
||||
// ASCII alphanumeric characters, for English, numbers: `Hello123`, etc.
|
||||
c.is_ascii_alphanumeric() ||
|
||||
// Latin script in Unicode for French, German, Spanish, etc.
|
||||
// Latin-1 Supplement
|
||||
// https://en.wikipedia.org/wiki/Latin-1_Supplement
|
||||
matches!(c, '\u{00C0}'..='\u{00FF}') ||
|
||||
// Latin Extended-A
|
||||
// https://en.wikipedia.org/wiki/Latin_Extended-A
|
||||
matches!(c, '\u{0100}'..='\u{017F}') ||
|
||||
// Latin Extended-B
|
||||
// https://en.wikipedia.org/wiki/Latin_Extended-B
|
||||
matches!(c, '\u{0180}'..='\u{024F}') ||
|
||||
// Cyrillic for Russian, Ukrainian, etc.
|
||||
// https://en.wikipedia.org/wiki/Cyrillic_script_in_Unicode
|
||||
matches!(c, '\u{0400}'..='\u{04FF}') ||
|
||||
// Some other known special characters that should be treated as word characters,
|
||||
// e.g. `a-b`, `var_name`, `I'm`, '@mention`, `#hashtag`, `100%`, `3.1415`, `2^3`, `a~b`, etc.
|
||||
matches!(c, '-' | '_' | '.' | '\'' | '$' | '%' | '@' | '#' | '^' | '~') ||
|
||||
// Characters that used in URL, e.g. `https://github.com/zed-industries/zed?a=1&b=2` for better wrapping a long URL.
|
||||
matches!(c, '/' | ':' | '?' | '&' | '=') ||
|
||||
// `⋯` character is special used in Zed, to keep this at the end of the line.
|
||||
matches!(c, '⋯')
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn width_for_char(&mut self, c: char) -> Pixels {
|
||||
if (c as u32) < 128 {
|
||||
@@ -219,6 +252,59 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_word_char() {
|
||||
#[track_caller]
|
||||
fn assert_word(word: &str) {
|
||||
for c in word.chars() {
|
||||
assert!(LineWrapper::is_word_char(c), "assertion failed for '{}'", c);
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_not_word(word: &str) {
|
||||
let found = word.chars().any(|c| !LineWrapper::is_word_char(c));
|
||||
assert!(found, "assertion failed for '{}'", word);
|
||||
}
|
||||
|
||||
assert_word("Hello123");
|
||||
assert_word("non-English");
|
||||
assert_word("var_name");
|
||||
assert_word("123456");
|
||||
assert_word("3.1415");
|
||||
assert_word("10^2");
|
||||
assert_word("1~2");
|
||||
assert_word("100%");
|
||||
assert_word("@mention");
|
||||
assert_word("#hashtag");
|
||||
assert_word("$variable");
|
||||
assert_word("more⋯");
|
||||
|
||||
// Space
|
||||
assert_not_word("foo bar");
|
||||
|
||||
// URL case
|
||||
assert_word("https://github.com/zed-industries/zed/");
|
||||
assert_word("github.com");
|
||||
assert_word("a=1&b=2");
|
||||
|
||||
// Latin-1 Supplement
|
||||
assert_word("ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏ");
|
||||
// Latin Extended-A
|
||||
assert_word("ĀāĂ㥹ĆćĈĉĊċČčĎď");
|
||||
// Latin Extended-B
|
||||
assert_word("ƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏ");
|
||||
// Cyrillic
|
||||
assert_word("АБВГДЕЖЗИЙКЛМНОП");
|
||||
|
||||
// non-word characters
|
||||
assert_not_word("你好");
|
||||
assert_not_word("안녕하세요");
|
||||
assert_not_word("こんにちは");
|
||||
assert_not_word("😀😁😂");
|
||||
assert_not_word("()[]{}<>");
|
||||
}
|
||||
|
||||
// For compatibility with the test macro
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate as gpui;
|
||||
|
||||
@@ -453,48 +453,17 @@ impl Frame {
|
||||
}
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
pub(crate) fn clear(&mut self) {
|
||||
{
|
||||
profiling::scope!("element_states clear");
|
||||
self.element_states.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("accessed_element_states clear");
|
||||
self.accessed_element_states.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("mouse_listeners clear");
|
||||
self.mouse_listeners.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("dispatch_tree clear");
|
||||
self.dispatch_tree.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("scene clear");
|
||||
self.scene.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("input handlers clear");
|
||||
self.input_handlers.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("tooltip_requests clear");
|
||||
self.tooltip_requests.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("cursor styles clear");
|
||||
self.cursor_styles.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("hitboxes clear");
|
||||
self.hitboxes.clear();
|
||||
}
|
||||
{
|
||||
profiling::scope!("deferred draws clear");
|
||||
self.deferred_draws.clear();
|
||||
}
|
||||
self.element_states.clear();
|
||||
self.accessed_element_states.clear();
|
||||
self.mouse_listeners.clear();
|
||||
self.dispatch_tree.clear();
|
||||
self.scene.clear();
|
||||
self.input_handlers.clear();
|
||||
self.tooltip_requests.clear();
|
||||
self.cursor_styles.clear();
|
||||
self.hitboxes.clear();
|
||||
self.deferred_draws.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn hit_test(&self, position: Point<Pixels>) -> HitTest {
|
||||
@@ -572,6 +541,7 @@ pub struct Window {
|
||||
appearance: WindowAppearance,
|
||||
appearance_observers: SubscriberSet<(), AnyObserver>,
|
||||
active: Rc<Cell<bool>>,
|
||||
hovered: Rc<Cell<bool>>,
|
||||
pub(crate) dirty: Rc<Cell<bool>>,
|
||||
pub(crate) needs_present: Rc<Cell<bool>>,
|
||||
pub(crate) last_input_timestamp: Rc<Cell<Instant>>,
|
||||
@@ -703,6 +673,7 @@ impl Window {
|
||||
let text_system = Arc::new(WindowTextSystem::new(cx.text_system().clone()));
|
||||
let dirty = Rc::new(Cell::new(true));
|
||||
let active = Rc::new(Cell::new(platform_window.is_active()));
|
||||
let hovered = Rc::new(Cell::new(platform_window.is_hovered()));
|
||||
let needs_present = Rc::new(Cell::new(false));
|
||||
let next_frame_callbacks: Rc<RefCell<Vec<FrameCallback>>> = Default::default();
|
||||
let last_input_timestamp = Rc::new(Cell::new(Instant::now()));
|
||||
@@ -726,7 +697,6 @@ impl Window {
|
||||
}
|
||||
}));
|
||||
platform_window.on_request_frame(Box::new({
|
||||
profiling::scope!("on_request_frame");
|
||||
let mut cx = cx.to_async();
|
||||
let dirty = dirty.clone();
|
||||
let active = active.clone();
|
||||
@@ -810,7 +780,17 @@ impl Window {
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
|
||||
platform_window.on_hover_status_change(Box::new({
|
||||
let mut cx = cx.to_async();
|
||||
move |active| {
|
||||
handle
|
||||
.update(&mut cx, |_, cx| {
|
||||
cx.window.hovered.set(active);
|
||||
cx.refresh();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}));
|
||||
platform_window.on_input({
|
||||
let mut cx = cx.to_async();
|
||||
Box::new(move |event| {
|
||||
@@ -861,6 +841,7 @@ impl Window {
|
||||
appearance,
|
||||
appearance_observers: SubscriberSet::new(),
|
||||
active,
|
||||
hovered,
|
||||
dirty,
|
||||
needs_present,
|
||||
last_input_timestamp,
|
||||
@@ -1254,6 +1235,17 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.active.get()
|
||||
}
|
||||
|
||||
/// Returns whether this window is considered to be the window
|
||||
/// that currently owns the mouse cursor.
|
||||
/// On mac, this is equivalent to `is_window_active`.
|
||||
pub fn is_window_hovered(&self) -> bool {
|
||||
if cfg!(target_os = "linux") {
|
||||
self.window.hovered.get()
|
||||
} else {
|
||||
self.is_window_active()
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle zoom on the window.
|
||||
pub fn zoom_window(&self) {
|
||||
self.window.platform_window.zoom();
|
||||
@@ -1461,7 +1453,6 @@ impl<'a> WindowContext<'a> {
|
||||
.next_frame
|
||||
.finish(&mut self.window.rendered_frame);
|
||||
ELEMENT_ARENA.with_borrow_mut(|element_arena| {
|
||||
profiling::scope!("element area clear");
|
||||
let percentage = (element_arena.len() as f32 / element_arena.capacity() as f32) * 100.;
|
||||
if percentage >= 80. {
|
||||
log::warn!("elevated element arena occupation: {}.", percentage);
|
||||
@@ -1472,47 +1463,37 @@ impl<'a> WindowContext<'a> {
|
||||
self.window.draw_phase = DrawPhase::Focus;
|
||||
let previous_focus_path = self.window.rendered_frame.focus_path();
|
||||
let previous_window_active = self.window.rendered_frame.window_active;
|
||||
{
|
||||
profiling::scope!("swapping frames");
|
||||
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
||||
}
|
||||
{
|
||||
profiling::scope!("clearing next frame");
|
||||
self.window.next_frame.clear();
|
||||
}
|
||||
mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame);
|
||||
self.window.next_frame.clear();
|
||||
let current_focus_path = self.window.rendered_frame.focus_path();
|
||||
let current_window_active = self.window.rendered_frame.window_active;
|
||||
|
||||
if previous_focus_path != current_focus_path
|
||||
|| previous_window_active != current_window_active
|
||||
{
|
||||
profiling::scope!("updating focus path");
|
||||
let current_focus_path = self.window.rendered_frame.focus_path();
|
||||
let current_window_active = self.window.rendered_frame.window_active;
|
||||
|
||||
if previous_focus_path != current_focus_path
|
||||
|| previous_window_active != current_window_active
|
||||
{
|
||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||
self.window
|
||||
.focus_lost_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(self));
|
||||
}
|
||||
|
||||
let event = WindowFocusEvent {
|
||||
previous_focus_path: if previous_window_active {
|
||||
previous_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
current_focus_path: if current_window_active {
|
||||
current_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
};
|
||||
if !previous_focus_path.is_empty() && current_focus_path.is_empty() {
|
||||
self.window
|
||||
.focus_listeners
|
||||
.focus_lost_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, self));
|
||||
.retain(&(), |listener| listener(self));
|
||||
}
|
||||
|
||||
let event = WindowFocusEvent {
|
||||
previous_focus_path: if previous_window_active {
|
||||
previous_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
current_focus_path: if current_window_active {
|
||||
current_focus_path
|
||||
} else {
|
||||
Default::default()
|
||||
},
|
||||
};
|
||||
self.window
|
||||
.focus_listeners
|
||||
.clone()
|
||||
.retain(&(), |listener| listener(&event, self));
|
||||
}
|
||||
|
||||
self.reset_cursor_style();
|
||||
@@ -1530,7 +1511,6 @@ impl<'a> WindowContext<'a> {
|
||||
profiling::finish_frame!();
|
||||
}
|
||||
|
||||
#[profiling::function]
|
||||
fn draw_roots(&mut self) {
|
||||
self.window.draw_phase = DrawPhase::Prepaint;
|
||||
self.window.tooltip_bounds.take();
|
||||
@@ -1898,7 +1878,6 @@ impl<'a> WindowContext<'a> {
|
||||
/// Updates the global element offset based on the given offset. This is used to implement
|
||||
/// drag handles and other manual painting of elements. This method should only be called during
|
||||
/// the prepaint phase of element drawing.
|
||||
#[profiling::function]
|
||||
pub fn with_absolute_element_offset<R>(
|
||||
&mut self,
|
||||
offset: Point<Pixels>,
|
||||
@@ -2764,7 +2743,6 @@ impl<'a> WindowContext<'a> {
|
||||
/// After calling it, you can request the bounds of the given layout node id or any descendant.
|
||||
///
|
||||
/// This method should only be called as part of the prepaint phase of element drawing.
|
||||
#[profiling::function]
|
||||
pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size<AvailableSpace>) {
|
||||
debug_assert_eq!(
|
||||
self.window.draw_phase,
|
||||
@@ -2773,10 +2751,7 @@ impl<'a> WindowContext<'a> {
|
||||
);
|
||||
|
||||
let mut layout_engine = self.window.layout_engine.take().unwrap();
|
||||
{
|
||||
profiling::scope!("layout_engine compute_layout");
|
||||
layout_engine.compute_layout(layout_id, available_space, self);
|
||||
}
|
||||
layout_engine.compute_layout(layout_id, available_space, self);
|
||||
self.window.layout_engine = Some(layout_engine);
|
||||
}
|
||||
|
||||
@@ -3029,7 +3004,7 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
fn reset_cursor_style(&self) {
|
||||
// Set the cursor only if we're the active window.
|
||||
if self.is_window_active() {
|
||||
if self.is_window_hovered() {
|
||||
let style = self
|
||||
.window
|
||||
.rendered_frame
|
||||
|
||||
@@ -16,4 +16,4 @@ doctest = false
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.66"
|
||||
quote = "1.0.9"
|
||||
syn = { version = "1.0.72", features = ["full"] }
|
||||
syn = { version = "1.0.72", features = ["full", "extra-traits"] }
|
||||
|
||||
@@ -1903,6 +1903,10 @@ impl Buffer {
|
||||
self.deferred_ops.insert(deferred_ops);
|
||||
}
|
||||
|
||||
pub fn has_deferred_ops(&self) -> bool {
|
||||
!self.deferred_ops.is_empty() || self.text.has_deferred_ops()
|
||||
}
|
||||
|
||||
fn can_apply_op(&self, operation: &Operation) -> bool {
|
||||
match operation {
|
||||
Operation::Buffer(_) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Handles conversions of `language` items to and from the [`rpc`] protocol.
|
||||
|
||||
use crate::{diagnostic_set::DiagnosticEntry, CursorShape, Diagnostic};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use clock::ReplicaId;
|
||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||
use rpc::proto;
|
||||
@@ -231,6 +231,21 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_anchor_range(range: Range<Anchor>) -> proto::AnchorRange {
|
||||
proto::AnchorRange {
|
||||
start: Some(serialize_anchor(&range.start)),
|
||||
end: Some(serialize_anchor(&range.end)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes an [`Range<Anchor>`] from the RPC representation.
|
||||
pub fn deserialize_anchor_range(range: proto::AnchorRange) -> Result<Range<Anchor>> {
|
||||
Ok(
|
||||
deserialize_anchor(range.start.context("invalid anchor")?).context("invalid anchor")?
|
||||
..deserialize_anchor(range.end.context("invalid anchor")?).context("invalid anchor")?,
|
||||
)
|
||||
}
|
||||
|
||||
// This behavior is currently copied in the collab database, for snapshotting channel notes
|
||||
/// Deserializes an [`crate::Operation`] from the RPC representation.
|
||||
pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operation> {
|
||||
|
||||
@@ -1207,7 +1207,7 @@ fn get_injections(
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
depth: usize,
|
||||
changed_ranges: &[Range<usize>],
|
||||
combined_injection_ranges: &mut HashMap<Arc<Language>, Vec<tree_sitter::Range>>,
|
||||
combined_injection_ranges: &mut HashMap<LanguageId, (Arc<Language>, Vec<tree_sitter::Range>)>,
|
||||
queue: &mut BinaryHeap<ParseStep>,
|
||||
) {
|
||||
let mut query_cursor = QueryCursorHandle::new();
|
||||
@@ -1223,7 +1223,7 @@ fn get_injections(
|
||||
.now_or_never()
|
||||
.and_then(|language| language.ok())
|
||||
{
|
||||
combined_injection_ranges.insert(language, Vec::new());
|
||||
combined_injection_ranges.insert(language.id, (language, Vec::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1276,8 +1276,9 @@ fn get_injections(
|
||||
if let Some(language) = language {
|
||||
if combined {
|
||||
combined_injection_ranges
|
||||
.entry(language.clone())
|
||||
.or_default()
|
||||
.entry(language.id)
|
||||
.or_insert_with(|| (language.clone(), vec![]))
|
||||
.1
|
||||
.extend(content_ranges);
|
||||
} else {
|
||||
queue.push(ParseStep {
|
||||
@@ -1303,7 +1304,7 @@ fn get_injections(
|
||||
}
|
||||
}
|
||||
|
||||
for (language, mut included_ranges) in combined_injection_ranges.drain() {
|
||||
for (_, (language, mut included_ranges)) in combined_injection_ranges.drain() {
|
||||
included_ranges.sort_unstable_by(|a, b| {
|
||||
Ord::cmp(&a.start_byte, &b.start_byte).then_with(|| Ord::cmp(&a.end_byte, &b.end_byte))
|
||||
});
|
||||
|
||||
@@ -518,7 +518,7 @@ impl ContextProvider for GoContextProvider {
|
||||
"test".into(),
|
||||
GO_PACKAGE_TASK_VARIABLE.template_value(),
|
||||
"-run".into(),
|
||||
format!("^{}$", VariableName::Symbol.template_value(),),
|
||||
format!("'^{}$'", VariableName::Symbol.template_value(),),
|
||||
],
|
||||
tags: vec!["go-test".to_owned()],
|
||||
..TaskTemplate::default()
|
||||
@@ -549,7 +549,7 @@ impl ContextProvider for GoContextProvider {
|
||||
"-v".into(),
|
||||
"-run".into(),
|
||||
format!(
|
||||
"^{}$/^{}$",
|
||||
"'^{}$/^{}$'",
|
||||
VariableName::Symbol.template_value(),
|
||||
GO_SUBTEST_NAME_TASK_VARIABLE.template_value(),
|
||||
),
|
||||
@@ -570,7 +570,7 @@ impl ContextProvider for GoContextProvider {
|
||||
"-benchmem".into(),
|
||||
"-run=^$".into(),
|
||||
"-bench".into(),
|
||||
format!("^{}$", VariableName::Symbol.template_value()),
|
||||
format!("'^{}$'", VariableName::Symbol.template_value()),
|
||||
],
|
||||
tags: vec!["go-benchmark".to_owned()],
|
||||
..TaskTemplate::default()
|
||||
|
||||
@@ -355,6 +355,9 @@ pub enum Event {
|
||||
},
|
||||
CollaboratorJoined(proto::PeerId),
|
||||
CollaboratorLeft(proto::PeerId),
|
||||
HostReshared,
|
||||
Reshared,
|
||||
Rejoined,
|
||||
RefreshInlayHints,
|
||||
RevealInProjectPanel(ProjectEntryId),
|
||||
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
|
||||
@@ -1716,6 +1719,7 @@ impl Project {
|
||||
self.shared_buffers.clear();
|
||||
self.set_collaborators_from_proto(message.collaborators, cx)?;
|
||||
self.metadata_changed(cx);
|
||||
cx.emit(Event::Reshared);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1753,6 +1757,7 @@ impl Project {
|
||||
.collect();
|
||||
self.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync)
|
||||
.unwrap();
|
||||
cx.emit(Event::Rejoined);
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
@@ -1805,9 +1810,11 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
self.client.send(proto::UnshareProject {
|
||||
project_id: remote_id,
|
||||
})?;
|
||||
self.client
|
||||
.send(proto::UnshareProject {
|
||||
project_id: remote_id,
|
||||
})
|
||||
.ok();
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -4099,6 +4106,7 @@ impl Project {
|
||||
return;
|
||||
}
|
||||
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let language_server_lookup_info: HashSet<(Model<Worktree>, Arc<Language>)> = buffers
|
||||
.into_iter()
|
||||
.filter_map(|buffer| {
|
||||
@@ -8810,6 +8818,7 @@ impl Project {
|
||||
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
|
||||
this.enqueue_buffer_ordered_message(BufferOrderedMessage::Resync)
|
||||
.unwrap();
|
||||
cx.emit(Event::HostReshared);
|
||||
}
|
||||
|
||||
cx.emit(Event::CollaboratorUpdated {
|
||||
@@ -11058,6 +11067,7 @@ async fn populate_labels_for_symbols(
|
||||
lsp_adapter: Option<Arc<CachedLspAdapter>>,
|
||||
output: &mut Vec<Symbol>,
|
||||
) {
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
let mut symbols_by_language = HashMap::<Option<Arc<Language>>, Vec<CoreSymbol>>::default();
|
||||
|
||||
let mut unknown_path = None;
|
||||
|
||||
@@ -444,11 +444,6 @@ mod test_inventory {
|
||||
|
||||
use super::{task_source_kind_preference, TaskSourceKind, UnboundedSender};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TestTask {
|
||||
name: String,
|
||||
}
|
||||
|
||||
pub(super) fn static_test_source(
|
||||
task_names: impl IntoIterator<Item = String>,
|
||||
updates: UnboundedSender<()>,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
fn main() {
|
||||
let mut build = prost_build::Config::new();
|
||||
build
|
||||
.type_attribute(".", "#[derive(serde::Serialize)]")
|
||||
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.compile_protos(&["proto/zed.proto"], &["proto"])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -255,7 +255,14 @@ message Envelope {
|
||||
TaskTemplates task_templates = 206;
|
||||
|
||||
LinkedEditingRange linked_editing_range = 209;
|
||||
LinkedEditingRangeResponse linked_editing_range_response = 210; // current max
|
||||
LinkedEditingRangeResponse linked_editing_range_response = 210;
|
||||
|
||||
AdvertiseContexts advertise_contexts = 211;
|
||||
OpenContext open_context = 212;
|
||||
OpenContextResponse open_context_response = 213;
|
||||
UpdateContext update_context = 214;
|
||||
SynchronizeContexts synchronize_contexts = 215;
|
||||
SynchronizeContextsResponse synchronize_contexts_response = 216; // current max
|
||||
}
|
||||
|
||||
reserved 158 to 161;
|
||||
@@ -2222,3 +2229,117 @@ message TaskSourceKind {
|
||||
string name = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ContextMessageStatus {
|
||||
oneof variant {
|
||||
Done done = 1;
|
||||
Pending pending = 2;
|
||||
Error error = 3;
|
||||
}
|
||||
|
||||
message Done {}
|
||||
|
||||
message Pending {}
|
||||
|
||||
message Error {
|
||||
string message = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message ContextMessage {
|
||||
LamportTimestamp id = 1;
|
||||
Anchor start = 2;
|
||||
LanguageModelRole role = 3;
|
||||
ContextMessageStatus status = 4;
|
||||
}
|
||||
|
||||
message SlashCommandOutputSection {
|
||||
AnchorRange range = 1;
|
||||
string icon_name = 2;
|
||||
string label = 3;
|
||||
}
|
||||
|
||||
message ContextOperation {
|
||||
oneof variant {
|
||||
InsertMessage insert_message = 1;
|
||||
UpdateMessage update_message = 2;
|
||||
UpdateSummary update_summary = 3;
|
||||
SlashCommandFinished slash_command_finished = 4;
|
||||
BufferOperation buffer_operation = 5;
|
||||
}
|
||||
|
||||
message InsertMessage {
|
||||
ContextMessage message = 1;
|
||||
repeated VectorClockEntry version = 2;
|
||||
}
|
||||
|
||||
message UpdateMessage {
|
||||
LamportTimestamp message_id = 1;
|
||||
LanguageModelRole role = 2;
|
||||
ContextMessageStatus status = 3;
|
||||
LamportTimestamp timestamp = 4;
|
||||
repeated VectorClockEntry version = 5;
|
||||
}
|
||||
|
||||
message UpdateSummary {
|
||||
string summary = 1;
|
||||
bool done = 2;
|
||||
LamportTimestamp timestamp = 3;
|
||||
repeated VectorClockEntry version = 4;
|
||||
}
|
||||
|
||||
message SlashCommandFinished {
|
||||
LamportTimestamp id = 1;
|
||||
AnchorRange output_range = 2;
|
||||
repeated SlashCommandOutputSection sections = 3;
|
||||
repeated VectorClockEntry version = 4;
|
||||
}
|
||||
|
||||
message BufferOperation {
|
||||
Operation operation = 1;
|
||||
}
|
||||
}
|
||||
|
||||
message Context {
|
||||
repeated ContextOperation operations = 1;
|
||||
}
|
||||
|
||||
message ContextMetadata {
|
||||
string context_id = 1;
|
||||
optional string summary = 2;
|
||||
}
|
||||
|
||||
message AdvertiseContexts {
|
||||
uint64 project_id = 1;
|
||||
repeated ContextMetadata contexts = 2;
|
||||
}
|
||||
|
||||
message OpenContext {
|
||||
uint64 project_id = 1;
|
||||
string context_id = 2;
|
||||
}
|
||||
|
||||
message OpenContextResponse {
|
||||
Context context = 1;
|
||||
}
|
||||
|
||||
message UpdateContext {
|
||||
uint64 project_id = 1;
|
||||
string context_id = 2;
|
||||
ContextOperation operation = 3;
|
||||
}
|
||||
|
||||
message ContextVersion {
|
||||
string context_id = 1;
|
||||
repeated VectorClockEntry context_version = 2;
|
||||
repeated VectorClockEntry buffer_version = 3;
|
||||
}
|
||||
|
||||
message SynchronizeContexts {
|
||||
uint64 project_id = 1;
|
||||
repeated ContextVersion contexts = 2;
|
||||
}
|
||||
|
||||
message SynchronizeContextsResponse {
|
||||
repeated ContextVersion contexts = 1;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ pub use error::*;
|
||||
pub use typed_envelope::*;
|
||||
|
||||
use collections::HashMap;
|
||||
pub use prost::Message;
|
||||
pub use prost::{DecodeError, Message};
|
||||
use serde::Serialize;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::time::Instant;
|
||||
@@ -337,7 +337,13 @@ messages!(
|
||||
(OpenNewBuffer, Foreground),
|
||||
(RestartLanguageServers, Foreground),
|
||||
(LinkedEditingRange, Background),
|
||||
(LinkedEditingRangeResponse, Background)
|
||||
(LinkedEditingRangeResponse, Background),
|
||||
(AdvertiseContexts, Foreground),
|
||||
(OpenContext, Foreground),
|
||||
(OpenContextResponse, Foreground),
|
||||
(UpdateContext, Foreground),
|
||||
(SynchronizeContexts, Foreground),
|
||||
(SynchronizeContextsResponse, Foreground),
|
||||
);
|
||||
|
||||
request_messages!(
|
||||
@@ -449,7 +455,9 @@ request_messages!(
|
||||
(DeleteDevServerProject, Ack),
|
||||
(RegenerateDevServerToken, RegenerateDevServerTokenResponse),
|
||||
(RenameDevServer, Ack),
|
||||
(RestartLanguageServers, Ack)
|
||||
(RestartLanguageServers, Ack),
|
||||
(OpenContext, OpenContextResponse),
|
||||
(SynchronizeContexts, SynchronizeContextsResponse),
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
@@ -511,6 +519,10 @@ entity_messages!(
|
||||
UpdateWorktree,
|
||||
UpdateWorktreeSettings,
|
||||
LspExtExpandMacro,
|
||||
AdvertiseContexts,
|
||||
OpenContext,
|
||||
UpdateContext,
|
||||
SynchronizeContexts,
|
||||
);
|
||||
|
||||
entity_messages!(
|
||||
|
||||
@@ -20,6 +20,7 @@ search.workspace = true
|
||||
settings.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
repl.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -20,8 +20,11 @@ use workspace::{
|
||||
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
};
|
||||
|
||||
mod repl_menu;
|
||||
|
||||
pub struct QuickActionBar {
|
||||
buffer_search_bar: View<BufferSearchBar>,
|
||||
repl_menu: Option<View<ContextMenu>>,
|
||||
toggle_settings_menu: Option<View<ContextMenu>>,
|
||||
toggle_selections_menu: Option<View<ContextMenu>>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
@@ -40,6 +43,7 @@ impl QuickActionBar {
|
||||
buffer_search_bar,
|
||||
toggle_settings_menu: None,
|
||||
toggle_selections_menu: None,
|
||||
repl_menu: None,
|
||||
active_item: None,
|
||||
_inlay_hints_enabled_subscription: None,
|
||||
workspace: workspace.weak_handle(),
|
||||
@@ -290,9 +294,13 @@ impl Render for QuickActionBar {
|
||||
.child(
|
||||
h_flex()
|
||||
.gap(Spacing::Medium.rems(cx))
|
||||
.children(self.render_repl_menu(cx))
|
||||
.children(editor_selections_dropdown)
|
||||
.child(editor_settings_dropdown),
|
||||
)
|
||||
.when_some(self.repl_menu.as_ref(), |el, repl_menu| {
|
||||
el.child(Self::render_menu_overlay(repl_menu))
|
||||
})
|
||||
.when_some(
|
||||
self.toggle_settings_menu.as_ref(),
|
||||
|el, toggle_settings_menu| {
|
||||
|
||||
116
crates/quick_action_bar/src/repl_menu.rs
Normal file
116
crates/quick_action_bar/src/repl_menu.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use gpui::AnyElement;
|
||||
use repl::{
|
||||
ExecutionState, JupyterSettings, Kernel, KernelSpecification, RuntimePanel, Session,
|
||||
SessionSupport,
|
||||
};
|
||||
use ui::{prelude::*, ButtonLike, IconWithIndicator, IntoElement, Tooltip};
|
||||
|
||||
use crate::QuickActionBar;
|
||||
|
||||
const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl";
|
||||
|
||||
impl QuickActionBar {
|
||||
pub fn render_repl_menu(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
|
||||
if !JupyterSettings::enabled(cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let workspace = self.workspace.upgrade()?.read(cx);
|
||||
|
||||
let (editor, repl_panel) = if let (Some(editor), Some(repl_panel)) =
|
||||
(self.active_editor(), workspace.panel::<RuntimePanel>(cx))
|
||||
{
|
||||
(editor, repl_panel)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let session = repl_panel.update(cx, |repl_panel, cx| {
|
||||
repl_panel.session(editor.downgrade(), cx)
|
||||
});
|
||||
|
||||
let session = match session {
|
||||
SessionSupport::ActiveSession(session) => session.read(cx),
|
||||
SessionSupport::Inactive(spec) => {
|
||||
return self.render_repl_launch_menu(spec, cx);
|
||||
}
|
||||
SessionSupport::RequiresSetup(language) => {
|
||||
return self.render_repl_setup(&language, cx);
|
||||
}
|
||||
SessionSupport::Unsupported => return None,
|
||||
};
|
||||
|
||||
let kernel_name: SharedString = session.kernel_specification.name.clone().into();
|
||||
let kernel_language: SharedString = session
|
||||
.kernel_specification
|
||||
.kernelspec
|
||||
.language
|
||||
.clone()
|
||||
.into();
|
||||
|
||||
let tooltip = |session: &Session| match &session.kernel {
|
||||
Kernel::RunningKernel(kernel) => match &kernel.execution_state {
|
||||
ExecutionState::Idle => {
|
||||
format!("Run code on {} ({})", kernel_name, kernel_language)
|
||||
}
|
||||
ExecutionState::Busy => format!("Interrupt {} ({})", kernel_name, kernel_language),
|
||||
},
|
||||
Kernel::StartingKernel(_) => format!("{} is starting", kernel_name),
|
||||
Kernel::ErroredLaunch(e) => format!("Error with kernel {}: {}", kernel_name, e),
|
||||
Kernel::ShuttingDown => format!("{} is shutting down", kernel_name),
|
||||
Kernel::Shutdown => "Nothing running".to_string(),
|
||||
};
|
||||
|
||||
let tooltip_text: SharedString = SharedString::from(tooltip(&session).clone());
|
||||
|
||||
let button = ButtonLike::new("toggle_repl_icon")
|
||||
.child(
|
||||
IconWithIndicator::new(Icon::new(IconName::Play), Some(session.kernel.dot()))
|
||||
.indicator_border_color(Some(cx.theme().colors().border)),
|
||||
)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx))
|
||||
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
|
||||
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
|
||||
.into_any_element();
|
||||
|
||||
Some(button)
|
||||
}
|
||||
|
||||
pub fn render_repl_launch_menu(
|
||||
&self,
|
||||
kernel_specification: KernelSpecification,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let tooltip: SharedString =
|
||||
SharedString::from(format!("Start REPL for {}", kernel_specification.name));
|
||||
|
||||
Some(
|
||||
IconButton::new("toggle_repl_icon", IconName::Play)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(|_, cx| cx.dispatch_action(Box::new(repl::Run {})))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_repl_setup(
|
||||
&self,
|
||||
language: &str,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language));
|
||||
Some(
|
||||
IconButton::new("toggle_repl_icon", IconName::Play)
|
||||
.size(ButtonSize::Compact)
|
||||
.icon_color(Color::Muted)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(|_, cx| cx.open_url(ZED_REPL_DOCUMENTATION))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -82,7 +82,7 @@ pub enum Kernel {
|
||||
}
|
||||
|
||||
impl Kernel {
|
||||
pub fn dot(&mut self) -> Indicator {
|
||||
pub fn dot(&self) -> Indicator {
|
||||
match self {
|
||||
Kernel::RunningKernel(kernel) => match kernel.execution_state {
|
||||
ExecutionState::Idle => Indicator::dot().color(Color::Success),
|
||||
|
||||
@@ -11,7 +11,11 @@ mod session;
|
||||
mod stdio;
|
||||
|
||||
pub use jupyter_settings::JupyterSettings;
|
||||
pub use runtime_panel::RuntimePanel;
|
||||
pub use kernels::{Kernel, KernelSpecification};
|
||||
pub use runtime_panel::Run;
|
||||
pub use runtime_panel::{RuntimePanel, SessionSupport};
|
||||
pub use runtimelib::ExecutionState;
|
||||
pub use session::Session;
|
||||
|
||||
fn zed_dispatcher(cx: &mut AppContext) -> impl Dispatcher {
|
||||
struct ZedDispatcher {
|
||||
|
||||
@@ -241,6 +241,17 @@ impl RuntimePanel {
|
||||
Some((selected_text, language_name, anchor_range))
|
||||
}
|
||||
|
||||
pub fn language(
|
||||
&self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Arc<str>> {
|
||||
match self.snippet(editor, cx) {
|
||||
Some((_, language, _)) => Some(language),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
|
||||
let kernel_specifications = kernel_specifications(self.fs.clone());
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
@@ -336,6 +347,50 @@ impl RuntimePanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SessionSupport {
|
||||
ActiveSession(View<Session>),
|
||||
Inactive(KernelSpecification),
|
||||
RequiresSetup(String),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl RuntimePanel {
|
||||
pub fn session(
|
||||
&mut self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> SessionSupport {
|
||||
let entity_id = editor.entity_id();
|
||||
let session = self.sessions.get(&entity_id).cloned();
|
||||
|
||||
match session {
|
||||
Some(session) => SessionSupport::ActiveSession(session),
|
||||
None => {
|
||||
let language = self.language(editor, cx);
|
||||
let language = match language {
|
||||
Some(language) => language,
|
||||
None => return SessionSupport::Unsupported,
|
||||
};
|
||||
// Check for kernelspec
|
||||
let kernelspec = self.kernelspec(&language, cx);
|
||||
|
||||
match kernelspec {
|
||||
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
|
||||
None => {
|
||||
let language: String = language.to_lowercase();
|
||||
// If no kernelspec but language is one of typescript, python, r, or julia
|
||||
// then we return RequiresSetup
|
||||
match language.as_str() {
|
||||
"typescript" | "python" => SessionSupport::RequiresSetup(language),
|
||||
_ => SessionSupport::Unsupported,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for RuntimePanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"RuntimePanel"
|
||||
|
||||
@@ -22,11 +22,11 @@ use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::{h_flex, prelude::*, v_flex, ButtonLike, ButtonStyle, Label};
|
||||
|
||||
pub struct Session {
|
||||
editor: WeakView<Editor>,
|
||||
kernel: Kernel,
|
||||
pub editor: WeakView<Editor>,
|
||||
pub kernel: Kernel,
|
||||
blocks: HashMap<String, EditorBlock>,
|
||||
messaging_task: Task<()>,
|
||||
kernel_specification: KernelSpecification,
|
||||
pub messaging_task: Task<()>,
|
||||
pub kernel_specification: KernelSpecification,
|
||||
}
|
||||
|
||||
struct EditorBlock {
|
||||
@@ -310,7 +310,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
fn interrupt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn interrupt(&mut self, cx: &mut ViewContext<Self>) {
|
||||
match &mut self.kernel {
|
||||
Kernel::RunningKernel(_kernel) => {
|
||||
self.send(InterruptRequest {}.into(), cx).ok();
|
||||
@@ -322,7 +322,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
fn shutdown(&mut self, cx: &mut ViewContext<Self>) {
|
||||
pub fn shutdown(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown);
|
||||
|
||||
match kernel {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use clock::ReplicaId;
|
||||
use collections::{BTreeMap, HashSet};
|
||||
|
||||
pub struct Network<T: Clone, R: rand::Rng> {
|
||||
inboxes: std::collections::BTreeMap<ReplicaId, Vec<Envelope<T>>>,
|
||||
all_messages: Vec<T>,
|
||||
inboxes: BTreeMap<ReplicaId, Vec<Envelope<T>>>,
|
||||
disconnected_peers: HashSet<ReplicaId>,
|
||||
rng: R,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
struct Envelope<T: Clone> {
|
||||
message: T,
|
||||
}
|
||||
@@ -14,8 +17,8 @@ struct Envelope<T: Clone> {
|
||||
impl<T: Clone, R: rand::Rng> Network<T, R> {
|
||||
pub fn new(rng: R) -> Self {
|
||||
Network {
|
||||
inboxes: Default::default(),
|
||||
all_messages: Vec::new(),
|
||||
inboxes: BTreeMap::default(),
|
||||
disconnected_peers: HashSet::default(),
|
||||
rng,
|
||||
}
|
||||
}
|
||||
@@ -24,6 +27,24 @@ impl<T: Clone, R: rand::Rng> Network<T, R> {
|
||||
self.inboxes.insert(id, Vec::new());
|
||||
}
|
||||
|
||||
pub fn disconnect_peer(&mut self, id: ReplicaId) {
|
||||
self.disconnected_peers.insert(id);
|
||||
self.inboxes.get_mut(&id).unwrap().clear();
|
||||
}
|
||||
|
||||
pub fn reconnect_peer(&mut self, id: ReplicaId, replicate_from: ReplicaId) {
|
||||
assert!(self.disconnected_peers.remove(&id));
|
||||
self.replicate(replicate_from, id);
|
||||
}
|
||||
|
||||
pub fn is_disconnected(&self, id: ReplicaId) -> bool {
|
||||
self.disconnected_peers.contains(&id)
|
||||
}
|
||||
|
||||
pub fn contains_disconnected_peers(&self) -> bool {
|
||||
!self.disconnected_peers.is_empty()
|
||||
}
|
||||
|
||||
pub fn replicate(&mut self, old_replica_id: ReplicaId, new_replica_id: ReplicaId) {
|
||||
self.inboxes
|
||||
.insert(new_replica_id, self.inboxes[&old_replica_id].clone());
|
||||
@@ -34,8 +55,13 @@ impl<T: Clone, R: rand::Rng> Network<T, R> {
|
||||
}
|
||||
|
||||
pub fn broadcast(&mut self, sender: ReplicaId, messages: Vec<T>) {
|
||||
// Drop messages from disconnected peers.
|
||||
if self.disconnected_peers.contains(&sender) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (replica, inbox) in self.inboxes.iter_mut() {
|
||||
if *replica != sender {
|
||||
if *replica != sender && !self.disconnected_peers.contains(replica) {
|
||||
for message in &messages {
|
||||
// Insert one or more duplicates of this message, potentially *before* the previous
|
||||
// message sent by this peer to simulate out-of-order delivery.
|
||||
@@ -51,7 +77,6 @@ impl<T: Clone, R: rand::Rng> Network<T, R> {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.all_messages.extend(messages);
|
||||
}
|
||||
|
||||
pub fn has_unreceived(&self, receiver: ReplicaId) -> bool {
|
||||
|
||||
@@ -1265,6 +1265,10 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_deferred_ops(&self) -> bool {
|
||||
!self.deferred_ops.is_empty()
|
||||
}
|
||||
|
||||
pub fn peek_undo_stack(&self) -> Option<&HistoryEntry> {
|
||||
self.history.undo_stack.last()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use gpui::{svg, AnimationElement, Hsla, IntoElement, Rems, Transformation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::EnumIter;
|
||||
use strum::{EnumIter, EnumString, IntoStaticStr};
|
||||
|
||||
use crate::{prelude::*, Indicator};
|
||||
|
||||
@@ -90,7 +90,9 @@ impl IconSize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Copy, Clone, EnumIter, Serialize, Deserialize)]
|
||||
#[derive(
|
||||
Debug, Eq, PartialEq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize,
|
||||
)]
|
||||
pub enum IconName {
|
||||
Ai,
|
||||
ArrowCircle,
|
||||
@@ -158,11 +160,11 @@ pub enum IconName {
|
||||
Font,
|
||||
FontSize,
|
||||
FontWeight,
|
||||
Github,
|
||||
GenericMinimize,
|
||||
GenericMaximize,
|
||||
GenericClose,
|
||||
GenericMaximize,
|
||||
GenericMinimize,
|
||||
GenericRestore,
|
||||
Github,
|
||||
Hash,
|
||||
HistoryRerun,
|
||||
Indicator,
|
||||
@@ -192,6 +194,10 @@ pub enum IconName {
|
||||
PullRequest,
|
||||
Quote,
|
||||
Regex,
|
||||
ReplPlay,
|
||||
ReplOff,
|
||||
ReplPause,
|
||||
ReplNeutral,
|
||||
Replace,
|
||||
ReplaceAll,
|
||||
ReplaceNext,
|
||||
@@ -229,12 +235,12 @@ pub enum IconName {
|
||||
Trash,
|
||||
TriangleRight,
|
||||
Update,
|
||||
Visible,
|
||||
WholeWord,
|
||||
XCircle,
|
||||
ZedAssistant,
|
||||
ZedAssistantFilled,
|
||||
ZedXCopilot,
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
@@ -306,11 +312,11 @@ impl IconName {
|
||||
IconName::Font => "icons/font.svg",
|
||||
IconName::FontSize => "icons/font_size.svg",
|
||||
IconName::FontWeight => "icons/font_weight.svg",
|
||||
IconName::Github => "icons/github.svg",
|
||||
IconName::GenericMinimize => "icons/generic_minimize.svg",
|
||||
IconName::GenericMaximize => "icons/generic_maximize.svg",
|
||||
IconName::GenericClose => "icons/generic_close.svg",
|
||||
IconName::GenericMaximize => "icons/generic_maximize.svg",
|
||||
IconName::GenericMinimize => "icons/generic_minimize.svg",
|
||||
IconName::GenericRestore => "icons/generic_restore.svg",
|
||||
IconName::Github => "icons/github.svg",
|
||||
IconName::Hash => "icons/hash.svg",
|
||||
IconName::HistoryRerun => "icons/history_rerun.svg",
|
||||
IconName::Indicator => "icons/indicator.svg",
|
||||
@@ -340,6 +346,10 @@ impl IconName {
|
||||
IconName::PullRequest => "icons/pull_request.svg",
|
||||
IconName::Quote => "icons/quote.svg",
|
||||
IconName::Regex => "icons/regex.svg",
|
||||
IconName::ReplPlay => "icons/repl_play.svg",
|
||||
IconName::ReplPause => "icons/repl_pause.svg",
|
||||
IconName::ReplNeutral => "icons/repl_neutral.svg",
|
||||
IconName::ReplOff => "icons/repl_off.svg",
|
||||
IconName::Replace => "icons/replace.svg",
|
||||
IconName::ReplaceAll => "icons/replace_all.svg",
|
||||
IconName::ReplaceNext => "icons/replace_next.svg",
|
||||
@@ -377,12 +387,12 @@ impl IconName {
|
||||
IconName::Trash => "icons/trash.svg",
|
||||
IconName::TriangleRight => "icons/triangle_right.svg",
|
||||
IconName::Update => "icons/update.svg",
|
||||
IconName::Visible => "icons/visible.svg",
|
||||
IconName::WholeWord => "icons/word_search.svg",
|
||||
IconName::XCircle => "icons/error.svg",
|
||||
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
||||
IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
|
||||
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||
IconName::Visible => "icons/visible.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.144.0"
|
||||
version = "0.145.0"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -54,27 +54,32 @@
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<caption>Zed with a large project open, showing language server and gitblame support</caption>
|
||||
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-1.png</image>
|
||||
<image>https://zed.dev/img/flatpak/flatpak-1.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>Zed with a file open and a channel message thread in the right sidebar</caption>
|
||||
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-2.png</image>
|
||||
<image>https://zed.dev/img/flatpak/flatpak-2.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>Example of a channel's shared document</caption>
|
||||
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-3.png</image>
|
||||
<image>https://zed.dev/img/flatpak/flatpak-3.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>Zed's extension list</caption>
|
||||
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-4.png</image>
|
||||
<image>https://zed.dev/img/flatpak/flatpak-4.png</image>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<caption>Theme switcher UI and example theme</caption>
|
||||
<image type="source" width="1122" height="859" xml:lang="en">https://zed.dev/img/flatpak/flatpak-5.png</image>
|
||||
<image>https://zed.dev/img/flatpak/flatpak-5.png</image>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
@release_info@
|
||||
@release_info@
|
||||
<release version="0.0.0" date="1970-01-01">
|
||||
<description>
|
||||
<p>Dummy release to keep flatpak-builder AppStream metadata validation from complaining</p>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
</component>
|
||||
|
||||
@@ -10,7 +10,7 @@ Exec=$APP_CLI $APP_ARGS
|
||||
Icon=$APP_ICON
|
||||
Categories=Utility;TextEditor;Development;IDE;
|
||||
Keywords=zed;
|
||||
MimeType=text/plain;inode/directory;
|
||||
MimeType=text/plain;inode/directory;x-scheme-handler/zed;
|
||||
|
||||
[Desktop Action NewWorkspace]
|
||||
Exec=$APP_CLI --new $APP_ARGS
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
- [Collaboration](./collaboration.md)
|
||||
- [Tasks](./tasks.md)
|
||||
- [Remote Development](./remote-development.md)
|
||||
- [Repl](./repl.md)
|
||||
|
||||
# Language Support
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ mkdir -p ~/.local
|
||||
# extract zed to ~/.local/zed.app/
|
||||
tar -xvf <path/to/download>.tar.gz -C ~/.local
|
||||
# link the zed binary to ~/.local/bin (or another directory in your $PATH)
|
||||
ln -sf ~/.local/bin/zed ~/.local/zed.app/bin/zed
|
||||
ln -sf ~/.local/zed.app/bin/zed ~/.local/bin/zed
|
||||
```
|
||||
|
||||
If you'd like integration with an XDG-compatible desktop environment, you will also need to install the `.desktop` file:
|
||||
|
||||
72
docs/src/repl.md
Normal file
72
docs/src/repl.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# REPL
|
||||
|
||||
Read. Eval. Print. Loop.
|
||||
|
||||
<div class="warning">
|
||||
|
||||
This feature is in active development. Details may change. We're delighted to get feedback as the REPL feature evolves.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
The built-in REPL for Zed allows you to run code interactively in your editor similarly to a notebook with your own text files.
|
||||
|
||||
<!-- TODO: Include GIF in action -->
|
||||
|
||||
To start using the REPL, add the following to your Zed `settings.json` to bring the power of [Jupyter kernels](https://docs.jupyter.org/en/latest/projects/kernels.html) to your editor:
|
||||
|
||||
```json
|
||||
{
|
||||
"jupyter": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After that, install any of the supported kernels:
|
||||
|
||||
* [Python](#python)
|
||||
* [TypeScript via Deno](#deno)
|
||||
|
||||
## Python
|
||||
|
||||
### Global environment
|
||||
|
||||
To setup your current python to have an available kernel, run:
|
||||
|
||||
```
|
||||
python -m ipykernel install --user
|
||||
```
|
||||
|
||||
### Conda Environment
|
||||
|
||||
```
|
||||
source activate myenv
|
||||
conda install ipykernel
|
||||
python -m ipykernel install --user --name myenv --display-name "Python (myenv)"
|
||||
```
|
||||
|
||||
|
||||
### Virtualenv with pip
|
||||
|
||||
```
|
||||
source activate myenv
|
||||
pip install ipykernel
|
||||
python -m ipykernel install --user --name myenv --display-name "Python (myenv)"
|
||||
```
|
||||
|
||||
## Deno
|
||||
|
||||
[Install Deno](https://docs.deno.com/runtime/manual/getting_started/installation/) and then install the Deno jupyter kernel:
|
||||
|
||||
```
|
||||
deno jupyter --unstable --install
|
||||
```
|
||||
|
||||
## Other languages
|
||||
|
||||
* [Julia](https://github.com/JuliaLang/IJulia.jl)
|
||||
* R
|
||||
- [Ark Kernel from Positron, formerly RStudio](https://github.com/posit-dev/ark)
|
||||
- [Xeus-R](https://github.com/jupyter-xeus/xeus-r)
|
||||
* [Scala](https://almond.sh/docs/quick-start-install)
|
||||
@@ -12,19 +12,19 @@ channel=$(<crates/zed/RELEASE_CHANNEL)
|
||||
export CHANNEL="$channel"
|
||||
export ARCHIVE="$archive"
|
||||
if [[ "$channel" == "dev" ]]; then
|
||||
export APP_ID="dev.zed.Zed-Dev"
|
||||
export APP_ID="dev.zed.ZedDev"
|
||||
export APP_NAME="Zed Devel"
|
||||
export BRANDING_LIGHT="#99c1f1"
|
||||
export BRANDING_DARK="#1a5fb4"
|
||||
export ICON_FILE="app-icon-dev"
|
||||
elif [[ "$channel" == "nightly" ]]; then
|
||||
export APP_ID="dev.zed.Zed-Nightly"
|
||||
export APP_ID="dev.zed.ZedNightly"
|
||||
export APP_NAME="Zed Nightly"
|
||||
export BRANDING_LIGHT="#e9aa6a"
|
||||
export BRANDING_DARK="#1a5fb4"
|
||||
export ICON_FILE="app-icon-nightly"
|
||||
elif [[ "$channel" == "preview" ]]; then
|
||||
export APP_ID="dev.zed.Zed-Preview"
|
||||
export APP_ID="dev.zed.ZedPreview"
|
||||
export APP_NAME="Zed Preview"
|
||||
export BRANDING_LIGHT="#99c1f1"
|
||||
export BRANDING_DARK="#1a5fb4"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Downloads the latest tarball from https://zed.dev/releases and unpacks it
|
||||
# into ~/.local/. If you'd prefer to do this manually, instructions are at
|
||||
# https://zed.dev/docs/linux.
|
||||
|
||||
main() {
|
||||
platform="$(uname -s)"
|
||||
arch="$(uname -m)"
|
||||
@@ -11,7 +15,6 @@ main() {
|
||||
platform="macos"
|
||||
elif [ "$platform" = "Linux" ]; then
|
||||
platform="linux"
|
||||
channel="${ZED_CHANNEL:-preview}"
|
||||
else
|
||||
echo "Unsupported platform $platform"
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user