Compare commits
82 Commits
collab-v0.
...
v0.65.0-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
923095a017 | ||
|
|
ccc8c247a1 | ||
|
|
8e6c5dbc3b | ||
|
|
3c53fcdb43 | ||
|
|
17dfbb91ba | ||
|
|
c3cf056fc5 | ||
|
|
275f0ae492 | ||
|
|
f4e9759f26 | ||
|
|
fdf758e050 | ||
|
|
0dfacd7ffa | ||
|
|
36c07f940c | ||
|
|
01929037f1 | ||
|
|
e401caff7c | ||
|
|
b222e8eb5a | ||
|
|
fb35631337 | ||
|
|
6659dac2e5 | ||
|
|
0dcdd6ea39 | ||
|
|
a66aa9c09c | ||
|
|
e6c5079a49 | ||
|
|
ee66adbb49 | ||
|
|
3612c46d6d | ||
|
|
bf9c9b0103 | ||
|
|
ea8778921b | ||
|
|
2ef2b5a053 | ||
|
|
5bb7701de7 | ||
|
|
b6f78cd5dc | ||
|
|
a6198c9a1a | ||
|
|
ad698fd110 | ||
|
|
d61c0fb24c | ||
|
|
3d5a3634cf | ||
|
|
9ad8731897 | ||
|
|
44c3cedc48 | ||
|
|
eeeaf6d9a2 | ||
|
|
2d4deaafcd | ||
|
|
c839ab2028 | ||
|
|
5d17347a45 | ||
|
|
9ce3524eb8 | ||
|
|
03115c8d71 | ||
|
|
dafdc4b4a5 | ||
|
|
05a6bd914d | ||
|
|
fb03eb7a3c | ||
|
|
8e70e1934a | ||
|
|
1bb41b6f54 | ||
|
|
90d1d9ac82 | ||
|
|
bed06346d1 | ||
|
|
7e02ac772a | ||
|
|
c0d67d9522 | ||
|
|
d14dd27cdc | ||
|
|
6b4dd2a5de | ||
|
|
9355d501bc | ||
|
|
335db5d03d | ||
|
|
98461ea0cd | ||
|
|
5707bae9b9 | ||
|
|
bbeb685769 | ||
|
|
cea103e47c | ||
|
|
ad31c284c7 | ||
|
|
738893c527 | ||
|
|
6da04d0eee | ||
|
|
7482660456 | ||
|
|
00123ffe2b | ||
|
|
53f8744794 | ||
|
|
537d4762f6 | ||
|
|
2f5004c238 | ||
|
|
7dcd6c920f | ||
|
|
ea42bc3c9b | ||
|
|
d3ba769291 | ||
|
|
3f1b95927f | ||
|
|
c183e854d7 | ||
|
|
86f51ade60 | ||
|
|
c838a7d973 | ||
|
|
9abfa037fd | ||
|
|
5efe2ed6d3 | ||
|
|
847376a4f5 | ||
|
|
1d6af4cf20 | ||
|
|
b6c5c7871e | ||
|
|
5acae094bd | ||
|
|
4d7425f4bf | ||
|
|
2497e7c008 | ||
|
|
474a5dd4f2 | ||
|
|
be6ee3cbff | ||
|
|
4977acf6a5 | ||
|
|
0cd2d9a9c8 |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -45,8 +45,11 @@ jobs:
|
||||
- name: Run tests
|
||||
run: cargo test --workspace --no-fail-fast
|
||||
|
||||
- name: Build collab binaries
|
||||
run: cargo build --bins --all-features
|
||||
- name: Build collab
|
||||
run: cargo build -p collab
|
||||
|
||||
- name: Build other binaries
|
||||
run: cargo build --workspace --bins --all-features
|
||||
|
||||
bundle:
|
||||
name: Bundle app
|
||||
|
||||
62
Cargo.lock
generated
62
Cargo.lock
generated
@@ -1028,7 +1028,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "collab"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1953,6 +1953,18 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bad48618fdb549078c333a7a8528acb57af271d0433bdecd523eb620628364e"
|
||||
|
||||
[[package]]
|
||||
name = "flume"
|
||||
version = "0.10.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project",
|
||||
"spin 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -3005,6 +3017,7 @@ dependencies = [
|
||||
"text",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"tree-sitter-embedded-template",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-javascript",
|
||||
"tree-sitter-json 0.19.0",
|
||||
@@ -3022,7 +3035,7 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
dependencies = [
|
||||
"spin",
|
||||
"spin 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4265,6 +4278,7 @@ name = "project_panel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"context_menu",
|
||||
"drag_and_drop",
|
||||
"editor",
|
||||
"futures 0.3.24",
|
||||
"gpui",
|
||||
@@ -4725,7 +4739,7 @@ dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin",
|
||||
"spin 0.5.2",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi 0.3.9",
|
||||
@@ -5563,6 +5577,15 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spsc-buffer"
|
||||
version = "0.1.1"
|
||||
@@ -5583,8 +5606,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -5593,8 +5615,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"atoi",
|
||||
@@ -5608,8 +5629,10 @@ dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
"event-listener",
|
||||
"flume",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-intrusive",
|
||||
"futures-util",
|
||||
"hashlink",
|
||||
@@ -5619,6 +5642,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"libc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"md-5",
|
||||
"memchr",
|
||||
@@ -5648,8 +5672,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
dependencies = [
|
||||
"dotenvy",
|
||||
"either",
|
||||
@@ -5657,6 +5680,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_json",
|
||||
"sha2 0.10.6",
|
||||
"sqlx-core",
|
||||
"sqlx-rt",
|
||||
@@ -5667,8 +5691,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "sqlx-rt"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396"
|
||||
source = "git+https://github.com/launchbadge/sqlx?rev=4b7053807c705df312bcb9b6281e184bf7534eb3#4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"tokio",
|
||||
@@ -6381,8 +6404,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.20.8"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=366210ae925d7ea0891bc7a0c738f60c77c04d7b#366210ae925d7ea0891bc7a0c738f60c77c04d7b"
|
||||
version = "0.20.9"
|
||||
source = "git+https://github.com/tree-sitter/tree-sitter?rev=36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da#36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
@@ -6426,6 +6449,16 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-embedded-template"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33817ade928c73a32d4f904a602321e09de9fc24b71d106f3b4b3f8ab30dcc38"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-go"
|
||||
version = "0.19.1"
|
||||
@@ -7640,7 +7673,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.64.0"
|
||||
version = "0.65.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
@@ -7719,6 +7752,7 @@ dependencies = [
|
||||
"tree-sitter-cpp",
|
||||
"tree-sitter-css",
|
||||
"tree-sitter-elixir",
|
||||
"tree-sitter-embedded-template",
|
||||
"tree-sitter-go",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-json 0.20.0",
|
||||
|
||||
@@ -65,7 +65,7 @@ serde_json = { version = "1.0", features = ["preserve_order", "raw_value"] }
|
||||
rand = { version = "0.8" }
|
||||
|
||||
[patch.crates-io]
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "366210ae925d7ea0891bc7a0c738f60c77c04d7b" }
|
||||
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "36b5b6c89e55ad1a502f8b3234bb3e12ec83a5da" }
|
||||
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"ctrl-n": "editor::MoveDown",
|
||||
"ctrl-b": "editor::MoveLeft",
|
||||
"ctrl-f": "editor::MoveRight",
|
||||
"ctrl-l": "editor::CenterScreen",
|
||||
"ctrl-l": "editor::NextScreen",
|
||||
"alt-left": "editor::MoveToPreviousWordStart",
|
||||
"alt-b": "editor::MoveToPreviousWordStart",
|
||||
"alt-right": "editor::MoveToNextWordEnd",
|
||||
@@ -472,6 +472,15 @@
|
||||
"terminal::SendText",
|
||||
"\u0001"
|
||||
],
|
||||
// Terminal.app compatability
|
||||
"alt-left": [
|
||||
"terminal::SendText",
|
||||
"\u001bb"
|
||||
],
|
||||
"alt-right": [
|
||||
"terminal::SendText",
|
||||
"\u001bf"
|
||||
],
|
||||
// There are conflicting bindings for these keys in the global context.
|
||||
// these bindings override them, remove at your own risk:
|
||||
"up": [
|
||||
|
||||
@@ -17,7 +17,6 @@ actions!(lsp_status, [ShowErrorMessage]);
|
||||
|
||||
const DOWNLOAD_ICON: &str = "icons/download_12.svg";
|
||||
const WARNING_ICON: &str = "icons/triangle_exclamation_12.svg";
|
||||
const DONE_ICON: &str = "icons/circle_check_12.svg";
|
||||
|
||||
pub enum Event {
|
||||
ShowError { lsp_name: Arc<str>, error: String },
|
||||
@@ -237,7 +236,6 @@ impl ActivityIndicator {
|
||||
|
||||
// Show any application auto-update info.
|
||||
if let Some(updater) = &self.auto_updater {
|
||||
// let theme = &cx.global::<Settings>().theme.workspace.status_bar;
|
||||
match &updater.read(cx).status() {
|
||||
AutoUpdateStatus::Checking => (
|
||||
Some(DOWNLOAD_ICON),
|
||||
@@ -254,9 +252,7 @@ impl ActivityIndicator {
|
||||
"Installing Zed update…".to_string(),
|
||||
None,
|
||||
),
|
||||
AutoUpdateStatus::Updated => {
|
||||
(Some(DONE_ICON), "Restart to update Zed".to_string(), None)
|
||||
}
|
||||
AutoUpdateStatus::Updated => (None, "Restart to update Zed".to_string(), None),
|
||||
AutoUpdateStatus::Errored => (
|
||||
Some(WARNING_ICON),
|
||||
"Auto update failed".to_string(),
|
||||
|
||||
@@ -1,820 +0,0 @@
|
||||
use super::{
|
||||
proto,
|
||||
user::{User, UserStore},
|
||||
Client, Status, Subscription, TypedEnvelope,
|
||||
};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use futures::lock::Mutex;
|
||||
use gpui::{
|
||||
AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakModelHandle,
|
||||
};
|
||||
use postage::prelude::Stream;
|
||||
use rand::prelude::*;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
mem,
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use sum_tree::{Bias, SumTree};
|
||||
use time::OffsetDateTime;
|
||||
use util::{post_inc, ResultExt as _, TryFutureExt};
|
||||
|
||||
pub struct ChannelList {
|
||||
available_channels: Option<Vec<ChannelDetails>>,
|
||||
channels: HashMap<u64, WeakModelHandle<Channel>>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_task: Task<Option<()>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ChannelDetails {
|
||||
pub id: u64,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct Channel {
|
||||
details: ChannelDetails,
|
||||
messages: SumTree<ChannelMessage>,
|
||||
loaded_all_messages: bool,
|
||||
next_pending_message_id: usize,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
outgoing_messages_lock: Arc<Mutex<()>>,
|
||||
rng: StdRng,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ChannelMessage {
|
||||
pub id: ChannelMessageId,
|
||||
pub body: String,
|
||||
pub timestamp: OffsetDateTime,
|
||||
pub sender: Arc<User>,
|
||||
pub nonce: u128,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChannelMessageId {
|
||||
Saved(u64),
|
||||
Pending(usize),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ChannelMessageSummary {
|
||||
max_id: ChannelMessageId,
|
||||
count: usize,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct Count(usize);
|
||||
|
||||
pub enum ChannelListEvent {}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ChannelEvent {
|
||||
MessagesUpdated {
|
||||
old_range: Range<usize>,
|
||||
new_count: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl Entity for ChannelList {
|
||||
type Event = ChannelListEvent;
|
||||
}
|
||||
|
||||
impl ChannelList {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _task = cx.spawn_weak(|this, mut cx| {
|
||||
let rpc = rpc.clone();
|
||||
async move {
|
||||
let mut status = rpc.status();
|
||||
while let Some((status, this)) = status.recv().await.zip(this.upgrade(&cx)) {
|
||||
match status {
|
||||
Status::Connected { .. } => {
|
||||
let response = rpc
|
||||
.request(proto::GetChannels {})
|
||||
.await
|
||||
.context("failed to fetch available channels")?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels =
|
||||
Some(response.channels.into_iter().map(Into::into).collect());
|
||||
|
||||
let mut to_remove = Vec::new();
|
||||
for (channel_id, channel) in &this.channels {
|
||||
if let Some(channel) = channel.upgrade(cx) {
|
||||
channel.update(cx, |channel, cx| channel.rejoin(cx))
|
||||
} else {
|
||||
to_remove.push(*channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
for channel_id in to_remove {
|
||||
this.channels.remove(&channel_id);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
Status::SignedOut { .. } => {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.available_channels = None;
|
||||
this.channels.clear();
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
});
|
||||
|
||||
Self {
|
||||
available_channels: None,
|
||||
channels: Default::default(),
|
||||
user_store,
|
||||
client: rpc,
|
||||
_task,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_channels(&self) -> Option<&[ChannelDetails]> {
|
||||
self.available_channels.as_deref()
|
||||
}
|
||||
|
||||
pub fn get_channel(
|
||||
&mut self,
|
||||
id: u64,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Option<ModelHandle<Channel>> {
|
||||
if let Some(channel) = self.channels.get(&id).and_then(|c| c.upgrade(cx)) {
|
||||
return Some(channel);
|
||||
}
|
||||
|
||||
let channels = self.available_channels.as_ref()?;
|
||||
let details = channels.iter().find(|details| details.id == id)?.clone();
|
||||
let channel = cx.add_model(|cx| {
|
||||
Channel::new(details, self.user_store.clone(), self.client.clone(), cx)
|
||||
});
|
||||
self.channels.insert(id, channel.downgrade());
|
||||
Some(channel)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Channel {
|
||||
type Event = ChannelEvent;
|
||||
|
||||
fn release(&mut self, _: &mut MutableAppContext) {
|
||||
self.rpc
|
||||
.send(proto::LeaveChannel {
|
||||
channel_id: self.details.id,
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
pub fn init(rpc: &Arc<Client>) {
|
||||
rpc.add_model_message_handler(Self::handle_message_sent);
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
details: ChannelDetails,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
rpc: Arc<Client>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let _subscription = rpc.add_model_for_remote_entity(details.id, cx);
|
||||
|
||||
{
|
||||
let user_store = user_store.clone();
|
||||
let rpc = rpc.clone();
|
||||
let channel_id = details.id;
|
||||
cx.spawn(|channel, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
channel.update(&mut cx, |channel, cx| {
|
||||
channel.insert_messages(messages, cx);
|
||||
channel.loaded_all_messages = loaded_all_messages;
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
Self {
|
||||
details,
|
||||
user_store,
|
||||
rpc,
|
||||
outgoing_messages_lock: Default::default(),
|
||||
messages: Default::default(),
|
||||
loaded_all_messages: false,
|
||||
next_pending_message_id: 0,
|
||||
rng: StdRng::from_entropy(),
|
||||
_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.details.name
|
||||
}
|
||||
|
||||
pub fn send_message(
|
||||
&mut self,
|
||||
body: String,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<Task<Result<()>>> {
|
||||
if body.is_empty() {
|
||||
Err(anyhow!("message body can't be empty"))?;
|
||||
}
|
||||
|
||||
let current_user = self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.current_user()
|
||||
.ok_or_else(|| anyhow!("current_user is not present"))?;
|
||||
|
||||
let channel_id = self.details.id;
|
||||
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
|
||||
let nonce = self.rng.gen();
|
||||
self.insert_messages(
|
||||
SumTree::from_item(
|
||||
ChannelMessage {
|
||||
id: pending_id,
|
||||
body: body.clone(),
|
||||
sender: current_user,
|
||||
timestamp: OffsetDateTime::now_utc(),
|
||||
nonce,
|
||||
},
|
||||
&(),
|
||||
),
|
||||
cx,
|
||||
);
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let outgoing_messages_lock = self.outgoing_messages_lock.clone();
|
||||
Ok(cx.spawn(|this, mut cx| async move {
|
||||
let outgoing_message_guard = outgoing_messages_lock.lock().await;
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body,
|
||||
nonce: Some(nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
drop(outgoing_message_guard);
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
Ok(())
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn load_more_messages(&mut self, cx: &mut ModelContext<Self>) -> bool {
|
||||
if !self.loaded_all_messages {
|
||||
let rpc = self.rpc.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let channel_id = self.details.id;
|
||||
if let Some(before_message_id) =
|
||||
self.messages.first().and_then(|message| match message.id {
|
||||
ChannelMessageId::Saved(id) => Some(id),
|
||||
ChannelMessageId::Pending(_) => None,
|
||||
})
|
||||
{
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc
|
||||
.request(proto::GetChannelMessages {
|
||||
channel_id,
|
||||
before_message_id,
|
||||
})
|
||||
.await?;
|
||||
let loaded_all_messages = response.done;
|
||||
let messages =
|
||||
messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
this.insert_messages(messages, cx);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn rejoin(&mut self, cx: &mut ModelContext<Self>) {
|
||||
let user_store = self.user_store.clone();
|
||||
let rpc = self.rpc.clone();
|
||||
let channel_id = self.details.id;
|
||||
cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let response = rpc.request(proto::JoinChannel { channel_id }).await?;
|
||||
let messages = messages_from_proto(response.messages, &user_store, &mut cx).await?;
|
||||
let loaded_all_messages = response.done;
|
||||
|
||||
let pending_messages = this.update(&mut cx, |this, cx| {
|
||||
if let Some((first_new_message, last_old_message)) =
|
||||
messages.first().zip(this.messages.last())
|
||||
{
|
||||
if first_new_message.id > last_old_message.id {
|
||||
let old_messages = mem::take(&mut this.messages);
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..old_messages.summary().count,
|
||||
new_count: 0,
|
||||
});
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
}
|
||||
|
||||
this.insert_messages(messages, cx);
|
||||
if loaded_all_messages {
|
||||
this.loaded_all_messages = loaded_all_messages;
|
||||
}
|
||||
|
||||
this.pending_messages().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
for pending_message in pending_messages {
|
||||
let request = rpc.request(proto::SendChannelMessage {
|
||||
channel_id,
|
||||
body: pending_message.body,
|
||||
nonce: Some(pending_message.nonce.into()),
|
||||
});
|
||||
let response = request.await?;
|
||||
let message = ChannelMessage::from_proto(
|
||||
response.message.ok_or_else(|| anyhow!("invalid message"))?,
|
||||
&user_store,
|
||||
&mut cx,
|
||||
)
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub fn message_count(&self) -> usize {
|
||||
self.messages.summary().count
|
||||
}
|
||||
|
||||
pub fn messages(&self) -> &SumTree<ChannelMessage> {
|
||||
&self.messages
|
||||
}
|
||||
|
||||
pub fn message(&self, ix: usize) -> &ChannelMessage {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(ix), Bias::Right, &());
|
||||
cursor.item().unwrap()
|
||||
}
|
||||
|
||||
pub fn messages_in_range(&self, range: Range<usize>) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<Count>();
|
||||
cursor.seek(&Count(range.start), Bias::Right, &());
|
||||
cursor.take(range.len())
|
||||
}
|
||||
|
||||
pub fn pending_messages(&self) -> impl Iterator<Item = &ChannelMessage> {
|
||||
let mut cursor = self.messages.cursor::<ChannelMessageId>();
|
||||
cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &());
|
||||
cursor
|
||||
}
|
||||
|
||||
async fn handle_message_sent(
|
||||
this: ModelHandle<Self>,
|
||||
message: TypedEnvelope<proto::ChannelMessageSent>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let user_store = this.read_with(&cx, |this, _| this.user_store.clone());
|
||||
let message = message
|
||||
.payload
|
||||
.message
|
||||
.ok_or_else(|| anyhow!("empty message"))?;
|
||||
|
||||
let message = ChannelMessage::from_proto(message, &user_store, &mut cx).await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.insert_messages(SumTree::from_item(message, &()), cx)
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn insert_messages(&mut self, messages: SumTree<ChannelMessage>, cx: &mut ModelContext<Self>) {
|
||||
if let Some((first_message, last_message)) = messages.first().zip(messages.last()) {
|
||||
let nonces = messages
|
||||
.cursor::<()>()
|
||||
.map(|m| m.nonce)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>();
|
||||
let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &());
|
||||
let start_ix = old_cursor.start().1 .0;
|
||||
let removed_messages = old_cursor.slice(&last_message.id, Bias::Right, &());
|
||||
let removed_count = removed_messages.summary().count;
|
||||
let new_count = messages.summary().count;
|
||||
let end_ix = start_ix + removed_count;
|
||||
|
||||
new_messages.push_tree(messages, &());
|
||||
|
||||
let mut ranges = Vec::<Range<usize>>::new();
|
||||
if new_messages.last().unwrap().is_pending() {
|
||||
new_messages.push_tree(old_cursor.suffix(&()), &());
|
||||
} else {
|
||||
new_messages.push_tree(
|
||||
old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()),
|
||||
&(),
|
||||
);
|
||||
|
||||
while let Some(message) = old_cursor.item() {
|
||||
let message_ix = old_cursor.start().1 .0;
|
||||
if nonces.contains(&message.nonce) {
|
||||
if ranges.last().map_or(false, |r| r.end == message_ix) {
|
||||
ranges.last_mut().unwrap().end += 1;
|
||||
} else {
|
||||
ranges.push(message_ix..message_ix + 1);
|
||||
}
|
||||
} else {
|
||||
new_messages.push(message.clone(), &());
|
||||
}
|
||||
old_cursor.next(&());
|
||||
}
|
||||
}
|
||||
|
||||
drop(old_cursor);
|
||||
self.messages = new_messages;
|
||||
|
||||
for range in ranges.into_iter().rev() {
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: range,
|
||||
new_count: 0,
|
||||
});
|
||||
}
|
||||
cx.emit(ChannelEvent::MessagesUpdated {
|
||||
old_range: start_ix..end_ix,
|
||||
new_count,
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn messages_from_proto(
|
||||
proto_messages: Vec<proto::ChannelMessage>,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<SumTree<ChannelMessage>> {
|
||||
let unique_user_ids = proto_messages
|
||||
.iter()
|
||||
.map(|m| m.sender_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect();
|
||||
user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_users(unique_user_ids, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut messages = Vec::with_capacity(proto_messages.len());
|
||||
for message in proto_messages {
|
||||
messages.push(ChannelMessage::from_proto(message, user_store, cx).await?);
|
||||
}
|
||||
let mut result = SumTree::new();
|
||||
result.extend(messages, &());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl From<proto::Channel> for ChannelDetails {
|
||||
fn from(message: proto::Channel) -> Self {
|
||||
Self {
|
||||
id: message.id,
|
||||
name: message.name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelMessage {
|
||||
pub async fn from_proto(
|
||||
message: proto::ChannelMessage,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let sender = user_store
|
||||
.update(cx, |user_store, cx| {
|
||||
user_store.get_user(message.sender_id, cx)
|
||||
})
|
||||
.await?;
|
||||
Ok(ChannelMessage {
|
||||
id: ChannelMessageId::Saved(message.id),
|
||||
body: message.body,
|
||||
timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64)?,
|
||||
sender,
|
||||
nonce: message
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce is required"))?
|
||||
.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_pending(&self) -> bool {
|
||||
matches!(self.id, ChannelMessageId::Pending(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Item for ChannelMessage {
|
||||
type Summary = ChannelMessageSummary;
|
||||
|
||||
fn summary(&self) -> Self::Summary {
|
||||
ChannelMessageSummary {
|
||||
max_id: self.id,
|
||||
count: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ChannelMessageId {
|
||||
fn default() -> Self {
|
||||
Self::Saved(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for ChannelMessageSummary {
|
||||
type Context = ();
|
||||
|
||||
fn add_summary(&mut self, summary: &Self, _: &()) {
|
||||
self.max_id = summary.max_id;
|
||||
self.count += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for ChannelMessageId {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
debug_assert!(summary.max_id > *self);
|
||||
*self = summary.max_id;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> sum_tree::Dimension<'a, ChannelMessageSummary> for Count {
|
||||
fn add_summary(&mut self, summary: &'a ChannelMessageSummary, _: &()) {
|
||||
self.0 += summary.count;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test::{FakeHttpClient, FakeServer};
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_channel_messages(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let user_id = 5;
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.update(|cx| Client::new(http_client.clone(), cx));
|
||||
let server = FakeServer::for_client(user_id, &client, cx).await;
|
||||
|
||||
Channel::init(&client);
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
|
||||
|
||||
let channel_list = cx.add_model(|cx| ChannelList::new(user_store, client.clone(), cx));
|
||||
channel_list.read_with(cx, |list, _| assert_eq!(list.available_channels(), None));
|
||||
|
||||
// Get the available channels.
|
||||
let get_channels = server.receive::<proto::GetChannels>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
get_channels.receipt(),
|
||||
proto::GetChannelsResponse {
|
||||
channels: vec![proto::Channel {
|
||||
id: 5,
|
||||
name: "the-channel".to_string(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
channel_list.next_notification(cx).await;
|
||||
channel_list.read_with(cx, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: 5,
|
||||
name: "the-channel".into(),
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![5]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 5,
|
||||
github_login: "nathansobo".into(),
|
||||
avatar_url: "http://avatar.com/nathansobo".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Join a channel and populate its existing messages.
|
||||
let channel = channel_list
|
||||
.update(cx, |list, cx| {
|
||||
let channel_id = list.available_channels().unwrap()[0].id;
|
||||
list.get_channel(channel_id, cx)
|
||||
})
|
||||
.unwrap();
|
||||
channel.read_with(cx, |channel, _| assert!(channel.messages().is_empty()));
|
||||
let join_channel = server.receive::<proto::JoinChannel>().await.unwrap();
|
||||
server
|
||||
.respond(
|
||||
join_channel.receipt(),
|
||||
proto::JoinChannelResponse {
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 10,
|
||||
body: "a".into(),
|
||||
timestamp: 1000,
|
||||
sender_id: 5,
|
||||
nonce: Some(1.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 11,
|
||||
body: "b".into(),
|
||||
timestamp: 1001,
|
||||
sender_id: 6,
|
||||
nonce: Some(2.into()),
|
||||
},
|
||||
],
|
||||
done: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Client requests all users for the received messages
|
||||
let mut get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
get_users.payload.user_ids.sort();
|
||||
assert_eq!(get_users.payload.user_ids, vec![6]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 6,
|
||||
github_login: "maxbrunsfeld".into(),
|
||||
avatar_url: "http://avatar.com/maxbrunsfeld".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "a".into()),
|
||||
("maxbrunsfeld".into(), "b".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Receive a new message.
|
||||
server.send(proto::ChannelMessageSent {
|
||||
channel_id: channel.read_with(cx, |channel, _| channel.details.id),
|
||||
message: Some(proto::ChannelMessage {
|
||||
id: 12,
|
||||
body: "c".into(),
|
||||
timestamp: 1002,
|
||||
sender_id: 7,
|
||||
nonce: Some(3.into()),
|
||||
}),
|
||||
});
|
||||
|
||||
// Client requests user for message since they haven't seen them yet
|
||||
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
assert_eq!(get_users.payload.user_ids, vec![7]);
|
||||
server
|
||||
.respond(
|
||||
get_users.receipt(),
|
||||
proto::UsersResponse {
|
||||
users: vec![proto::User {
|
||||
id: 7,
|
||||
github_login: "as-cii".into(),
|
||||
avatar_url: "http://avatar.com/as-cii".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 2..2,
|
||||
new_count: 1,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(2..3)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[("as-cii".into(), "c".into())]
|
||||
)
|
||||
});
|
||||
|
||||
// Scroll up to view older messages.
|
||||
channel.update(cx, |channel, cx| {
|
||||
assert!(channel.load_more_messages(cx));
|
||||
});
|
||||
let get_messages = server.receive::<proto::GetChannelMessages>().await.unwrap();
|
||||
assert_eq!(get_messages.payload.channel_id, 5);
|
||||
assert_eq!(get_messages.payload.before_message_id, 10);
|
||||
server
|
||||
.respond(
|
||||
get_messages.receipt(),
|
||||
proto::GetChannelMessagesResponse {
|
||||
done: true,
|
||||
messages: vec![
|
||||
proto::ChannelMessage {
|
||||
id: 8,
|
||||
body: "y".into(),
|
||||
timestamp: 998,
|
||||
sender_id: 5,
|
||||
nonce: Some(4.into()),
|
||||
},
|
||||
proto::ChannelMessage {
|
||||
id: 9,
|
||||
body: "z".into(),
|
||||
timestamp: 999,
|
||||
sender_id: 6,
|
||||
nonce: Some(5.into()),
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
channel.next_event(cx).await,
|
||||
ChannelEvent::MessagesUpdated {
|
||||
old_range: 0..0,
|
||||
new_count: 2,
|
||||
}
|
||||
);
|
||||
channel.read_with(cx, |channel, _| {
|
||||
assert_eq!(
|
||||
channel
|
||||
.messages_in_range(0..2)
|
||||
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("nathansobo".into(), "y".into()),
|
||||
("maxbrunsfeld".into(), "z".into())
|
||||
]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
pub mod channel;
|
||||
pub mod http;
|
||||
pub mod telemetry;
|
||||
pub mod user;
|
||||
@@ -44,7 +43,6 @@ use thiserror::Error;
|
||||
use url::Url;
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
|
||||
pub use channel::*;
|
||||
pub use rpc::*;
|
||||
pub use user::*;
|
||||
|
||||
|
||||
@@ -32,7 +32,6 @@ pub struct Telemetry {
|
||||
struct TelemetryState {
|
||||
metrics_id: Option<Arc<str>>,
|
||||
device_id: Option<Arc<str>>,
|
||||
app: &'static str,
|
||||
app_version: Option<Arc<str>>,
|
||||
release_channel: Option<&'static str>,
|
||||
os_version: Option<Arc<str>>,
|
||||
@@ -80,8 +79,6 @@ struct MixpanelEventProperties {
|
||||
app_version: Option<Arc<str>>,
|
||||
#[serde(rename = "Signed In")]
|
||||
signed_in: bool,
|
||||
#[serde(rename = "App")]
|
||||
app: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -120,7 +117,6 @@ impl Telemetry {
|
||||
state: Mutex::new(TelemetryState {
|
||||
os_version: platform.os_version().ok().map(|v| v.to_string().into()),
|
||||
os_name: platform.os_name().into(),
|
||||
app: "Zed",
|
||||
app_version: platform.app_version().ok().map(|v| v.to_string().into()),
|
||||
release_channel,
|
||||
device_id: None,
|
||||
@@ -205,7 +201,11 @@ impl Telemetry {
|
||||
let json_bytes = serde_json::to_vec(&[MixpanelEngageRequest {
|
||||
token,
|
||||
distinct_id: device_id,
|
||||
set: json!({ "Staff": is_staff, "ID": metrics_id }),
|
||||
set: json!({
|
||||
"Staff": is_staff,
|
||||
"ID": metrics_id,
|
||||
"App": true
|
||||
}),
|
||||
}])?;
|
||||
let request = Request::post(MIXPANEL_ENGAGE_URL)
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -241,7 +241,6 @@ impl Telemetry {
|
||||
release_channel: state.release_channel,
|
||||
app_version: state.app_version.clone(),
|
||||
signed_in: state.metrics_id.is_some(),
|
||||
app: state.app,
|
||||
},
|
||||
};
|
||||
state.queue.push(event);
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
|
||||
default-run = "collab"
|
||||
edition = "2021"
|
||||
name = "collab"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
|
||||
[[bin]]
|
||||
name = "collab"
|
||||
@@ -50,8 +50,9 @@ tracing-log = "0.1.3"
|
||||
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
|
||||
|
||||
[dependencies.sqlx]
|
||||
version = "0.6"
|
||||
features = ["runtime-tokio-rustls", "postgres", "time", "uuid"]
|
||||
git = "https://github.com/launchbadge/sqlx"
|
||||
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
features = ["runtime-tokio-rustls", "postgres", "json", "time", "uuid"]
|
||||
|
||||
[dev-dependencies]
|
||||
collections = { path = "../collections", features = ["test-support"] }
|
||||
@@ -78,5 +79,10 @@ lazy_static = "1.4"
|
||||
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||
unindent = "0.1"
|
||||
|
||||
[dev-dependencies.sqlx]
|
||||
git = "https://github.com/launchbadge/sqlx"
|
||||
rev = "4b7053807c705df312bcb9b6281e184bf7534eb3"
|
||||
features = ["sqlite"]
|
||||
|
||||
[features]
|
||||
seed-support = ["clap", "lipsum", "reqwest"]
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
CREATE TABLE IF NOT EXISTS "users" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"github_login" VARCHAR,
|
||||
"admin" BOOLEAN,
|
||||
"email_address" VARCHAR(255) DEFAULT NULL,
|
||||
"invite_code" VARCHAR(64),
|
||||
"invite_count" INTEGER NOT NULL DEFAULT 0,
|
||||
"inviter_id" INTEGER REFERENCES users (id),
|
||||
"connected_once" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT now,
|
||||
"metrics_id" VARCHAR(255),
|
||||
"github_user_id" INTEGER
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_users_github_login" ON "users" ("github_login");
|
||||
CREATE UNIQUE INDEX "index_invite_code_users" ON "users" ("invite_code");
|
||||
CREATE INDEX "index_users_on_email_address" ON "users" ("email_address");
|
||||
CREATE INDEX "index_users_on_github_user_id" ON "users" ("github_user_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "access_tokens" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"user_id" INTEGER REFERENCES users (id),
|
||||
"hash" VARCHAR(128)
|
||||
);
|
||||
CREATE INDEX "index_access_tokens_user_id" ON "access_tokens" ("user_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "contacts" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"user_id_a" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"user_id_b" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"a_to_b" BOOLEAN NOT NULL,
|
||||
"should_notify" BOOLEAN NOT NULL,
|
||||
"accepted" BOOLEAN NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX "index_contacts_user_ids" ON "contacts" ("user_id_a", "user_id_b");
|
||||
CREATE INDEX "index_contacts_user_id_b" ON "contacts" ("user_id_b");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "projects" (
|
||||
"id" INTEGER PRIMARY KEY,
|
||||
"host_user_id" INTEGER REFERENCES users (id) NOT NULL,
|
||||
"unregistered" BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
auth,
|
||||
db::{Invite, NewUserParams, ProjectId, Signup, User, UserId, WaitlistSummary},
|
||||
db::{Invite, NewUserParams, Signup, User, UserId, WaitlistSummary},
|
||||
rpc::{self, ResultExt},
|
||||
AppState, Error, Result,
|
||||
};
|
||||
@@ -16,9 +16,7 @@ use axum::{
|
||||
};
|
||||
use axum_extra::response::ErasedJson;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use std::sync::Arc;
|
||||
use tower::ServiceBuilder;
|
||||
use tracing::instrument;
|
||||
|
||||
@@ -32,16 +30,6 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
|
||||
.route("/invite_codes/:code", get(get_user_for_invite_code))
|
||||
.route("/panic", post(trace_panic))
|
||||
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
|
||||
.route(
|
||||
"/user_activity/summary",
|
||||
get(get_top_users_activity_summary),
|
||||
)
|
||||
.route(
|
||||
"/user_activity/timeline/:user_id",
|
||||
get(get_user_activity_timeline),
|
||||
)
|
||||
.route("/user_activity/counts", get(get_active_user_counts))
|
||||
.route("/project_metadata", get(get_project_metadata))
|
||||
.route("/signups", post(create_signup))
|
||||
.route("/signups_summary", get(get_waitlist_summary))
|
||||
.route("/user_invites", post(create_invite_from_code))
|
||||
@@ -283,93 +271,6 @@ async fn get_rpc_server_snapshot(
|
||||
Ok(ErasedJson::pretty(rpc_server.snapshot().await))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TimePeriodParams {
|
||||
#[serde(with = "time::serde::iso8601")]
|
||||
start: OffsetDateTime,
|
||||
#[serde(with = "time::serde::iso8601")]
|
||||
end: OffsetDateTime,
|
||||
}
|
||||
|
||||
async fn get_top_users_activity_summary(
|
||||
Query(params): Query<TimePeriodParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let summary = app
|
||||
.db
|
||||
.get_top_users_activity_summary(params.start..params.end, 100)
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(summary))
|
||||
}
|
||||
|
||||
async fn get_user_activity_timeline(
|
||||
Path(user_id): Path<i32>,
|
||||
Query(params): Query<TimePeriodParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let summary = app
|
||||
.db
|
||||
.get_user_activity_timeline(params.start..params.end, UserId(user_id))
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(summary))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ActiveUserCountParams {
|
||||
#[serde(flatten)]
|
||||
period: TimePeriodParams,
|
||||
durations_in_minutes: String,
|
||||
#[serde(default)]
|
||||
only_collaborative: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ActiveUserSet {
|
||||
active_time_in_minutes: u64,
|
||||
user_count: usize,
|
||||
}
|
||||
|
||||
async fn get_active_user_counts(
|
||||
Query(params): Query<ActiveUserCountParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let durations_in_minutes = params.durations_in_minutes.split(',');
|
||||
let mut user_sets = Vec::new();
|
||||
for duration in durations_in_minutes {
|
||||
let duration = duration
|
||||
.parse()
|
||||
.map_err(|_| anyhow!("invalid duration: {duration}"))?;
|
||||
user_sets.push(ActiveUserSet {
|
||||
active_time_in_minutes: duration,
|
||||
user_count: app
|
||||
.db
|
||||
.get_active_user_count(
|
||||
params.period.start..params.period.end,
|
||||
Duration::from_secs(duration * 60),
|
||||
params.only_collaborative,
|
||||
)
|
||||
.await?,
|
||||
})
|
||||
}
|
||||
Ok(ErasedJson::pretty(user_sets))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct GetProjectMetadataParams {
|
||||
project_id: u64,
|
||||
}
|
||||
|
||||
async fn get_project_metadata(
|
||||
Query(params): Query<GetProjectMetadataParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<ErasedJson> {
|
||||
let extensions = app
|
||||
.db
|
||||
.get_project_extensions(ProjectId::from_proto(params.project_id))
|
||||
.await?;
|
||||
Ok(ErasedJson::pretty(json!({ "extensions": extensions })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateAccessTokenQueryParams {
|
||||
public_key: String,
|
||||
@@ -437,7 +338,7 @@ async fn create_signup(
|
||||
Json(params): Json<Signup>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<()> {
|
||||
app.db.create_signup(params).await?;
|
||||
app.db.create_signup(¶ms).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ pub async fn validate_header<B>(mut req: Request<B>, next: Next<B>) -> impl Into
|
||||
|
||||
const MAX_ACCESS_TOKENS_TO_STORE: usize = 8;
|
||||
|
||||
pub async fn create_access_token(db: &dyn db::Db, user_id: UserId) -> Result<String> {
|
||||
pub async fn create_access_token(db: &db::DefaultDb, user_id: UserId) -> Result<String> {
|
||||
let access_token = rpc::auth::random_token();
|
||||
let access_token_hash =
|
||||
hash_access_token(&access_token).context("failed to hash access token")?;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use collab::{Error, Result};
|
||||
use db::{Db, PostgresDb, UserId};
|
||||
use rand::prelude::*;
|
||||
use db::DefaultDb;
|
||||
use serde::{de::DeserializeOwned, Deserialize};
|
||||
use std::fmt::Write;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
#[allow(unused)]
|
||||
#[path = "../db.rs"]
|
||||
@@ -18,9 +16,8 @@ struct GitHubUser {
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let database_url = std::env::var("DATABASE_URL").expect("missing DATABASE_URL env var");
|
||||
let db = PostgresDb::new(&database_url, 5)
|
||||
let db = DefaultDb::new(&database_url, 5)
|
||||
.await
|
||||
.expect("failed to connect to postgres database");
|
||||
let github_token = std::env::var("GITHUB_TOKEN").expect("missing GITHUB_TOKEN env var");
|
||||
@@ -64,16 +61,14 @@ async fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
let mut zed_user_ids = Vec::<UserId>::new();
|
||||
for (github_user, admin) in zed_users {
|
||||
if let Some(user) = db
|
||||
if db
|
||||
.get_user_by_github_account(&github_user.login, Some(github_user.id))
|
||||
.await
|
||||
.expect("failed to fetch user")
|
||||
.is_none()
|
||||
{
|
||||
zed_user_ids.push(user.id);
|
||||
} else if let Some(email) = &github_user.email {
|
||||
zed_user_ids.push(
|
||||
if let Some(email) = &github_user.email {
|
||||
db.create_user(
|
||||
email,
|
||||
admin,
|
||||
@@ -84,11 +79,8 @@ async fn main() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("failed to insert user")
|
||||
.user_id,
|
||||
);
|
||||
} else if admin {
|
||||
zed_user_ids.push(
|
||||
.expect("failed to insert user");
|
||||
} else if admin {
|
||||
db.create_user(
|
||||
&format!("{}@zed.dev", github_user.login),
|
||||
admin,
|
||||
@@ -99,62 +91,10 @@ async fn main() {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("failed to insert user")
|
||||
.user_id,
|
||||
);
|
||||
.expect("failed to insert user");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let zed_org_id = if let Some(org) = db
|
||||
.find_org_by_slug("zed")
|
||||
.await
|
||||
.expect("failed to fetch org")
|
||||
{
|
||||
org.id
|
||||
} else {
|
||||
db.create_org("Zed", "zed")
|
||||
.await
|
||||
.expect("failed to insert org")
|
||||
};
|
||||
|
||||
let general_channel_id = if let Some(channel) = db
|
||||
.get_org_channels(zed_org_id)
|
||||
.await
|
||||
.expect("failed to fetch channels")
|
||||
.iter()
|
||||
.find(|c| c.name == "General")
|
||||
{
|
||||
channel.id
|
||||
} else {
|
||||
let channel_id = db
|
||||
.create_org_channel(zed_org_id, "General")
|
||||
.await
|
||||
.expect("failed to insert channel");
|
||||
|
||||
let now = OffsetDateTime::now_utc();
|
||||
let max_seconds = Duration::days(100).as_seconds_f64();
|
||||
let mut timestamps = (0..1000)
|
||||
.map(|_| now - Duration::seconds_f64(rng.gen_range(0_f64..=max_seconds)))
|
||||
.collect::<Vec<_>>();
|
||||
timestamps.sort();
|
||||
for timestamp in timestamps {
|
||||
let sender_id = *zed_user_ids.choose(&mut rng).unwrap();
|
||||
let body = lipsum::lipsum_words(rng.gen_range(1..=50));
|
||||
db.create_channel_message(channel_id, sender_id, &body, timestamp, rng.gen())
|
||||
.await
|
||||
.expect("failed to insert message");
|
||||
}
|
||||
channel_id
|
||||
};
|
||||
|
||||
for user_id in zed_user_ids {
|
||||
db.add_org_member(zed_org_id, user_id, true)
|
||||
.await
|
||||
.expect("failed to insert org membership");
|
||||
db.add_channel_member(general_channel_id, user_id, true)
|
||||
.await
|
||||
.expect("failed to insert channel membership");
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_github<T: DeserializeOwned>(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,14 @@
|
||||
use crate::{
|
||||
db::{NewUserParams, ProjectId, TestDb, UserId},
|
||||
rpc::{Executor, Server, Store},
|
||||
db::{NewUserParams, ProjectId, SqliteTestDb as TestDb, UserId},
|
||||
rpc::{Executor, Server},
|
||||
AppState,
|
||||
};
|
||||
use ::rpc::Peer;
|
||||
use anyhow::anyhow;
|
||||
use call::{room, ActiveCall, ParticipantLocation, Room};
|
||||
use client::{
|
||||
self, test::FakeHttpClient, Channel, ChannelDetails, ChannelList, Client, Connection,
|
||||
Credentials, EstablishConnectionError, PeerId, User, UserStore, RECEIVE_TIMEOUT,
|
||||
self, test::FakeHttpClient, Client, Connection, Credentials, EstablishConnectionError, PeerId,
|
||||
User, UserStore, RECEIVE_TIMEOUT,
|
||||
};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -16,10 +16,7 @@ use editor::{
|
||||
ToggleCodeActions, Undo,
|
||||
};
|
||||
use fs::{FakeFs, Fs as _, HomeDir, LineEnding};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
Future, StreamExt as _,
|
||||
};
|
||||
use futures::{channel::oneshot, Future, StreamExt as _};
|
||||
use gpui::{
|
||||
executor::{self, Deterministic},
|
||||
geometry::vector::vec2f,
|
||||
@@ -39,7 +36,6 @@ use project::{
|
||||
use rand::prelude::*;
|
||||
use serde_json::json;
|
||||
use settings::{Formatter, Settings};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
env, mem,
|
||||
@@ -73,7 +69,10 @@ async fn test_basic_calls(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -259,6 +258,8 @@ async fn test_basic_calls(
|
||||
pending: Default::default()
|
||||
}
|
||||
);
|
||||
|
||||
eprintln!("finished test {:?}", start.elapsed());
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -271,7 +272,7 @@ async fn test_room_uniqueness(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let _client_a2 = server.create_client(cx_a2, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -376,7 +377,7 @@ async fn test_leaving_room_on_disconnection(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -505,7 +506,7 @@ async fn test_calls_on_multiple_connections(
|
||||
cx_b2: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b1 = server.create_client(cx_b1, "user_b").await;
|
||||
let client_b2 = server.create_client(cx_b2, "user_b").await;
|
||||
@@ -654,7 +655,7 @@ async fn test_share_project(
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let (_, window_b) = cx_b.add_window(|_| EmptyView);
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -791,7 +792,7 @@ async fn test_unshare_project(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -874,7 +875,7 @@ async fn test_host_disconnect(
|
||||
) {
|
||||
cx_b.update(editor::init);
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -908,7 +909,7 @@ async fn test_host_disconnect(
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "b.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "b.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -979,7 +980,7 @@ async fn test_active_call_events(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
@@ -1068,7 +1069,7 @@ async fn test_room_location(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
client_a.fs.insert_tree("/a", json!({})).await;
|
||||
@@ -1234,7 +1235,7 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -1409,7 +1410,7 @@ async fn test_git_diff_base_change(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -1661,7 +1662,7 @@ async fn test_fs_operations(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
executor.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -1927,7 +1928,7 @@ async fn test_fs_operations(
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -1981,7 +1982,7 @@ async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_buffer_reloading(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2040,7 +2041,7 @@ async fn test_editing_while_guest_opens_buffer(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2087,7 +2088,7 @@ async fn test_leaving_worktree_while_opening_buffer(
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2132,7 +2133,7 @@ async fn test_canceling_buffer_opening(
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2183,7 +2184,7 @@ async fn test_leaving_project(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -2316,7 +2317,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -2581,7 +2582,7 @@ async fn test_collaborating_with_diagnostics(
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2755,7 +2756,7 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2848,7 +2849,7 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
|
||||
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
use project::FormatTrigger;
|
||||
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -2949,7 +2950,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3093,7 +3094,7 @@ async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3194,7 +3195,7 @@ async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3273,7 +3274,7 @@ async fn test_project_search(cx_a: &mut TestAppContext, cx_b: &mut TestAppContex
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3375,7 +3376,7 @@ async fn test_document_highlights(cx_a: &mut TestAppContext, cx_b: &mut TestAppC
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3478,7 +3479,7 @@ async fn test_lsp_hover(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3586,7 +3587,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
mut rng: StdRng,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3662,7 +3663,7 @@ async fn test_collaborating_with_code_actions(
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
cx_b.update(editor::init);
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3704,7 +3705,7 @@ async fn test_collaborating_with_code_actions(
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), true, cx)
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -3873,7 +3874,7 @@ async fn test_collaborating_with_code_actions(
|
||||
async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
cx_b.update(editor::init);
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -3925,7 +3926,7 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
cx_b.add_window(|cx| Workspace::new(project_b.clone(), |_, _| unimplemented!(), cx));
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "one.rs"), true, cx)
|
||||
workspace.open_path((worktree_id, "one.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -4065,7 +4066,7 @@ async fn test_language_server_statuses(
|
||||
deterministic.forbid_parking();
|
||||
|
||||
cx_b.update(editor::init);
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -4169,415 +4170,6 @@ async fn test_language_server_statuses(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_basic_chat(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
// Create an org that includes these 2 users.
|
||||
let db = &server.app_state.db;
|
||||
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
|
||||
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a channel that includes all the users.
|
||||
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
|
||||
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.create_channel_message(
|
||||
channel_id,
|
||||
client_b.current_user_id(cx_b),
|
||||
"hello A, it's B.",
|
||||
OffsetDateTime::now_utc(),
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels_a =
|
||||
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
|
||||
channels_a
|
||||
.condition(cx_a, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
channels_a.read_with(cx_a, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
let channel_a = channels_a.update(cx_a, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_a
|
||||
.condition(cx_a, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
let channels_b =
|
||||
cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
|
||||
channels_b
|
||||
.condition(cx_b, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
channels_b.read_with(cx_b, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let channel_b = channels_b.update(cx_b, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel
|
||||
.send_message("oh, hi B.".to_string(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
let task = channel.send_message("sup".to_string(), cx).unwrap();
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
&[
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), true),
|
||||
("user_a".to_string(), "sup".to_string(), true)
|
||||
]
|
||||
);
|
||||
task
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
server
|
||||
.store()
|
||||
.await
|
||||
.channel(channel_id)
|
||||
.unwrap()
|
||||
.connection_ids
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
cx_b.update(|_| drop(channel_b));
|
||||
server
|
||||
.condition(|state| state.channel(channel_id).unwrap().connection_ids.len() == 1)
|
||||
.await;
|
||||
|
||||
cx_a.update(|_| drop(channel_a));
|
||||
server
|
||||
.condition(|state| state.channel(channel_id).is_none())
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_chat_message_validation(cx_a: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
|
||||
let db = &server.app_state.db;
|
||||
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
|
||||
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
|
||||
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels_a =
|
||||
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
|
||||
channels_a
|
||||
.condition(cx_a, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
let channel_a = channels_a.update(cx_a, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
|
||||
// Messages aren't allowed to be too long.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
let long_body = "this is long.\n".repeat(1024);
|
||||
channel.send_message(long_body, cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Messages aren't allowed to be blank.
|
||||
channel_a.update(cx_a, |channel, cx| {
|
||||
channel.send_message(String::new(), cx).unwrap_err()
|
||||
});
|
||||
|
||||
// Leading and trailing whitespace are trimmed.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel
|
||||
.send_message("\n surrounded by whitespace \n".to_string(), cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_channel_messages(channel_id, 10, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|m| &m.body)
|
||||
.collect::<Vec<_>>(),
|
||||
&["surrounded by whitespace"]
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_chat_reconnection(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
let mut status_b = client_b.status();
|
||||
|
||||
// Create an org that includes these 2 users.
|
||||
let db = &server.app_state.db;
|
||||
let org_id = db.create_org("Test Org", "test-org").await.unwrap();
|
||||
db.add_org_member(org_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_org_member(org_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Create a channel that includes all the users.
|
||||
let channel_id = db.create_org_channel(org_id, "test-channel").await.unwrap();
|
||||
db.add_channel_member(channel_id, client_a.current_user_id(cx_a), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_channel_member(channel_id, client_b.current_user_id(cx_b), false)
|
||||
.await
|
||||
.unwrap();
|
||||
db.create_channel_message(
|
||||
channel_id,
|
||||
client_b.current_user_id(cx_b),
|
||||
"hello A, it's B.",
|
||||
OffsetDateTime::now_utc(),
|
||||
2,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let channels_a =
|
||||
cx_a.add_model(|cx| ChannelList::new(client_a.user_store.clone(), client_a.clone(), cx));
|
||||
channels_a
|
||||
.condition(cx_a, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
|
||||
channels_a.read_with(cx_a, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
let channel_a = channels_a.update(cx_a, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_a.read_with(cx_a, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_a
|
||||
.condition(cx_a, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
let channels_b =
|
||||
cx_b.add_model(|cx| ChannelList::new(client_b.user_store.clone(), client_b.clone(), cx));
|
||||
channels_b
|
||||
.condition(cx_b, |list, _| list.available_channels().is_some())
|
||||
.await;
|
||||
channels_b.read_with(cx_b, |list, _| {
|
||||
assert_eq!(
|
||||
list.available_channels().unwrap(),
|
||||
&[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
|
||||
let channel_b = channels_b.update(cx_b, |this, cx| {
|
||||
this.get_channel(channel_id.to_proto(), cx).unwrap()
|
||||
});
|
||||
channel_b.read_with(cx_b, |channel, _| assert!(channel.messages().is_empty()));
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
})
|
||||
.await;
|
||||
|
||||
// Disconnect client B, ensuring we can still access its cached channel data.
|
||||
server.forbid_connections();
|
||||
server.disconnect_client(client_b.peer_id().unwrap());
|
||||
cx_b.foreground().advance_clock(rpc::RECEIVE_TIMEOUT);
|
||||
while !matches!(
|
||||
status_b.next().await,
|
||||
Some(client::Status::ReconnectionError { .. })
|
||||
) {}
|
||||
|
||||
channels_b.read_with(cx_b, |channels, _| {
|
||||
assert_eq!(
|
||||
channels.available_channels().unwrap(),
|
||||
[ChannelDetails {
|
||||
id: channel_id.to_proto(),
|
||||
name: "test-channel".to_string()
|
||||
}]
|
||||
)
|
||||
});
|
||||
channel_b.read_with(cx_b, |channel, _| {
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
[("user_b".to_string(), "hello A, it's B.".to_string(), false)]
|
||||
)
|
||||
});
|
||||
|
||||
// Send a message from client B while it is disconnected.
|
||||
channel_b
|
||||
.update(cx_b, |channel, cx| {
|
||||
let task = channel
|
||||
.send_message("can you see this?".to_string(), cx)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
&[
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), true)
|
||||
]
|
||||
);
|
||||
task
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
// Send a message from client A while B is disconnected.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel
|
||||
.send_message("oh, hi B.".to_string(), cx)
|
||||
.unwrap()
|
||||
.detach();
|
||||
let task = channel.send_message("sup".to_string(), cx).unwrap();
|
||||
assert_eq!(
|
||||
channel_messages(channel),
|
||||
&[
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), true),
|
||||
("user_a".to_string(), "sup".to_string(), true)
|
||||
]
|
||||
);
|
||||
task
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Give client B a chance to reconnect.
|
||||
server.allow_connections();
|
||||
cx_b.foreground().advance_clock(Duration::from_secs(10));
|
||||
|
||||
// Verify that B sees the new messages upon reconnection, as well as the message client B
|
||||
// sent while offline.
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
|
||||
// Ensure client A and B can communicate normally after reconnection.
|
||||
channel_a
|
||||
.update(cx_a, |channel, cx| {
|
||||
channel.send_message("you online?".to_string(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
channel_b
|
||||
.condition(cx_b, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), false),
|
||||
("user_a".to_string(), "you online?".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
|
||||
channel_b
|
||||
.update(cx_b, |channel, cx| {
|
||||
channel.send_message("yep".to_string(), cx).unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
channel_a
|
||||
.condition(cx_a, |channel, _| {
|
||||
channel_messages(channel)
|
||||
== [
|
||||
("user_b".to_string(), "hello A, it's B.".to_string(), false),
|
||||
("user_a".to_string(), "oh, hi B.".to_string(), false),
|
||||
("user_a".to_string(), "sup".to_string(), false),
|
||||
("user_b".to_string(), "can you see this?".to_string(), false),
|
||||
("user_a".to_string(), "you online?".to_string(), false),
|
||||
("user_b".to_string(), "yep".to_string(), false),
|
||||
]
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_contacts(
|
||||
deterministic: Arc<Deterministic>,
|
||||
@@ -4586,7 +4178,7 @@ async fn test_contacts(
|
||||
cx_c: &mut TestAppContext,
|
||||
) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
let client_c = server.create_client(cx_c, "user_c").await;
|
||||
@@ -4912,7 +4504,7 @@ async fn test_contact_requests(
|
||||
cx_a.foreground().forbid_parking();
|
||||
|
||||
// Connect to a server as 3 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_a2 = server.create_client(cx_a2, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
@@ -5093,7 +4685,7 @@ async fn test_following(
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -5134,7 +4726,7 @@ async fn test_following(
|
||||
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5142,7 +4734,7 @@ async fn test_following(
|
||||
.unwrap();
|
||||
let editor_a2 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5153,7 +4745,7 @@ async fn test_following(
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b1 = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5367,7 +4959,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -5411,7 +5003,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5423,7 +5015,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
|
||||
let _editor_b1 = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5474,7 +5066,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
|
||||
workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "3.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "3.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5485,7 +5077,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
assert_eq!(*workspace.active_pane(), pane_b1);
|
||||
workspace.open_path((worktree_id, "4.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "4.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5545,7 +5137,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
cx_b.update(editor::init);
|
||||
|
||||
// 2 clients connect to a server.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -5586,7 +5178,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let _editor_a1 = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "1.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "1.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
@@ -5699,7 +5291,7 @@ async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppCont
|
||||
// When client B activates a different item in the original pane, it automatically stops following client A.
|
||||
workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "2.txt"), true, cx)
|
||||
workspace.open_path((worktree_id, "2.txt"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -5719,7 +5311,7 @@ async fn test_peers_simultaneously_following_each_other(
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let mut server = TestServer::start(cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
@@ -5789,7 +5381,7 @@ async fn test_random_collaboration(
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let mut server = TestServer::start(cx.foreground(), cx.background()).await;
|
||||
let mut server = TestServer::start(cx.background()).await;
|
||||
let db = server.app_state.db.clone();
|
||||
|
||||
let mut available_guests = Vec::new();
|
||||
@@ -5987,6 +5579,13 @@ async fn test_random_collaboration(
|
||||
guest_client.username,
|
||||
id
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.abs_path(),
|
||||
host_snapshot.abs_path(),
|
||||
"{} has different abs path than the host for worktree {}",
|
||||
guest_client.username,
|
||||
id
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false).collect::<Vec<_>>(),
|
||||
@@ -6076,8 +5675,6 @@ struct TestServer {
|
||||
peer: Arc<Peer>,
|
||||
app_state: Arc<AppState>,
|
||||
server: Arc<Server>,
|
||||
foreground: Rc<executor::Foreground>,
|
||||
notifications: mpsc::UnboundedReceiver<()>,
|
||||
connection_killers: Arc<Mutex<HashMap<PeerId, Arc<AtomicBool>>>>,
|
||||
forbid_connections: Arc<AtomicBool>,
|
||||
_test_db: TestDb,
|
||||
@@ -6085,13 +5682,10 @@ struct TestServer {
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
async fn start(
|
||||
foreground: Rc<executor::Foreground>,
|
||||
background: Arc<executor::Background>,
|
||||
) -> Self {
|
||||
async fn start(background: Arc<executor::Background>) -> Self {
|
||||
static NEXT_LIVE_KIT_SERVER_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
|
||||
let test_db = TestDb::fake(background.clone());
|
||||
let test_db = TestDb::new(background.clone());
|
||||
let live_kit_server_id = NEXT_LIVE_KIT_SERVER_ID.fetch_add(1, SeqCst);
|
||||
let live_kit_server = live_kit_client::TestServer::create(
|
||||
format!("http://livekit.{}.test", live_kit_server_id),
|
||||
@@ -6102,14 +5696,11 @@ impl TestServer {
|
||||
.unwrap();
|
||||
let app_state = Self::build_app_state(&test_db, &live_kit_server).await;
|
||||
let peer = Peer::new();
|
||||
let notifications = mpsc::unbounded();
|
||||
let server = Server::new(app_state.clone(), Some(notifications.0));
|
||||
let server = Server::new(app_state.clone());
|
||||
Self {
|
||||
peer,
|
||||
app_state,
|
||||
server,
|
||||
foreground,
|
||||
notifications: notifications.1,
|
||||
connection_killers: Default::default(),
|
||||
forbid_connections: Default::default(),
|
||||
_test_db: test_db,
|
||||
@@ -6147,7 +5738,7 @@ impl TestServer {
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("creating user failed")
|
||||
.user_id
|
||||
};
|
||||
let client_name = name.to_string();
|
||||
@@ -6187,7 +5778,11 @@ impl TestServer {
|
||||
let (client_conn, server_conn, killed) =
|
||||
Connection::in_memory(cx.background());
|
||||
let (connection_id_tx, connection_id_rx) = oneshot::channel();
|
||||
let user = db.get_user_by_id(user_id).await.unwrap().unwrap();
|
||||
let user = db
|
||||
.get_user_by_id(user_id)
|
||||
.await
|
||||
.expect("retrieving user failed")
|
||||
.unwrap();
|
||||
cx.background()
|
||||
.spawn(server.handle_connection(
|
||||
server_conn,
|
||||
@@ -6221,7 +5816,6 @@ impl TestServer {
|
||||
default_item_factory: |_, _| unimplemented!(),
|
||||
});
|
||||
|
||||
Channel::init(&client);
|
||||
Project::init(&client);
|
||||
cx.update(|cx| {
|
||||
workspace::init(app_state.clone(), cx);
|
||||
@@ -6322,21 +5916,6 @@ impl TestServer {
|
||||
config: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn condition<F>(&mut self, mut predicate: F)
|
||||
where
|
||||
F: FnMut(&Store) -> bool,
|
||||
{
|
||||
assert!(
|
||||
self.foreground.parking_forbidden(),
|
||||
"you must call forbid_parking to use server conditions so we don't block indefinitely"
|
||||
);
|
||||
while !(predicate)(&*self.server.store.lock().await) {
|
||||
self.foreground.start_waiting();
|
||||
self.notifications.next().await;
|
||||
self.foreground.finish_waiting();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TestServer {
|
||||
@@ -7052,20 +6631,6 @@ impl Executor for Arc<gpui::executor::Background> {
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_messages(channel: &Channel) -> Vec<(String, String, bool)> {
|
||||
channel
|
||||
.messages()
|
||||
.cursor::<()>()
|
||||
.map(|m| {
|
||||
(
|
||||
m.sender.github_login.clone(),
|
||||
m.body.clone(),
|
||||
m.is_pending(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct RoomParticipants {
|
||||
remote: Vec<String>,
|
||||
|
||||
@@ -13,12 +13,12 @@ use crate::rpc::ResultExt as _;
|
||||
use anyhow::anyhow;
|
||||
use axum::{routing::get, Router};
|
||||
use collab::{Error, Result};
|
||||
use db::{Db, PostgresDb};
|
||||
use db::DefaultDb as Db;
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
env::args,
|
||||
net::{SocketAddr, TcpListener},
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
@@ -49,14 +49,14 @@ pub struct MigrateConfig {
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
db: Arc<dyn Db>,
|
||||
db: Arc<Db>,
|
||||
live_kit_client: Option<Arc<dyn live_kit_server::api::Client>>,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
async fn new(config: Config) -> Result<Arc<Self>> {
|
||||
let db = PostgresDb::new(&config.database_url, 5).await?;
|
||||
let db = Db::new(&config.database_url, 5).await?;
|
||||
let live_kit_client = if let Some(((server, key), secret)) = config
|
||||
.live_kit_server
|
||||
.as_ref()
|
||||
@@ -96,13 +96,12 @@ async fn main() -> Result<()> {
|
||||
}
|
||||
Some("migrate") => {
|
||||
let config = envy::from_env::<MigrateConfig>().expect("error loading config");
|
||||
let db = PostgresDb::new(&config.database_url, 5).await?;
|
||||
let db = Db::new(&config.database_url, 5).await?;
|
||||
|
||||
let migrations_path = config
|
||||
.migrations_path
|
||||
.as_deref()
|
||||
.or(db::DEFAULT_MIGRATIONS_PATH.map(|s| s.as_ref()))
|
||||
.ok_or_else(|| anyhow!("missing MIGRATIONS_PATH environment variable"))?;
|
||||
.unwrap_or_else(|| Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")));
|
||||
|
||||
let migrations = db.migrate(&migrations_path, false).await?;
|
||||
for (migration, duration) in migrations {
|
||||
@@ -122,9 +121,7 @@ async fn main() -> Result<()> {
|
||||
let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port))
|
||||
.expect("failed to bind TCP listener");
|
||||
|
||||
let rpc_server = rpc::Server::new(state.clone(), None);
|
||||
rpc_server
|
||||
.start_recording_project_activity(Duration::from_secs(5 * 60), rpc::RealExecutor);
|
||||
let rpc_server = rpc::Server::new(state.clone());
|
||||
|
||||
let app = api::routes(rpc_server.clone(), state.clone())
|
||||
.merge(rpc::routes(rpc_server.clone()))
|
||||
|
||||
@@ -2,7 +2,7 @@ mod store;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{self, ChannelId, MessageId, ProjectId, User, UserId},
|
||||
db::{self, ProjectId, User, UserId},
|
||||
AppState, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -24,7 +24,7 @@ use axum::{
|
||||
};
|
||||
use collections::{HashMap, HashSet};
|
||||
use futures::{
|
||||
channel::{mpsc, oneshot},
|
||||
channel::oneshot,
|
||||
future::{self, BoxFuture},
|
||||
stream::FuturesUnordered,
|
||||
FutureExt, SinkExt, StreamExt, TryStreamExt,
|
||||
@@ -42,7 +42,6 @@ use std::{
|
||||
marker::PhantomData,
|
||||
net::SocketAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
os::unix::prelude::OsStrExt,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
@@ -51,7 +50,6 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
pub use store::{Store, Worktree};
|
||||
use time::OffsetDateTime;
|
||||
use tokio::{
|
||||
sync::{Mutex, MutexGuard},
|
||||
time::Sleep,
|
||||
@@ -62,10 +60,6 @@ use tracing::{info_span, instrument, Instrument};
|
||||
lazy_static! {
|
||||
static ref METRIC_CONNECTIONS: IntGauge =
|
||||
register_int_gauge!("connections", "number of connections").unwrap();
|
||||
static ref METRIC_REGISTERED_PROJECTS: IntGauge =
|
||||
register_int_gauge!("registered_projects", "number of registered projects").unwrap();
|
||||
static ref METRIC_ACTIVE_PROJECTS: IntGauge =
|
||||
register_int_gauge!("active_projects", "number of active projects").unwrap();
|
||||
static ref METRIC_SHARED_PROJECTS: IntGauge = register_int_gauge!(
|
||||
"shared_projects",
|
||||
"number of open projects with one or more guests"
|
||||
@@ -95,7 +89,6 @@ pub struct Server {
|
||||
pub(crate) store: Mutex<Store>,
|
||||
app_state: Arc<AppState>,
|
||||
handlers: HashMap<TypeId, MessageHandler>,
|
||||
notifications: Option<mpsc::UnboundedSender<()>>,
|
||||
}
|
||||
|
||||
pub trait Executor: Send + Clone {
|
||||
@@ -107,9 +100,6 @@ pub trait Executor: Send + Clone {
|
||||
#[derive(Clone)]
|
||||
pub struct RealExecutor;
|
||||
|
||||
const MESSAGE_COUNT_PER_PAGE: usize = 100;
|
||||
const MAX_MESSAGE_LEN: usize = 1024;
|
||||
|
||||
pub(crate) struct StoreGuard<'a> {
|
||||
guard: MutexGuard<'a, Store>,
|
||||
_not_send: PhantomData<Rc<()>>,
|
||||
@@ -132,16 +122,12 @@ where
|
||||
}
|
||||
|
||||
impl Server {
|
||||
pub fn new(
|
||||
app_state: Arc<AppState>,
|
||||
notifications: Option<mpsc::UnboundedSender<()>>,
|
||||
) -> Arc<Self> {
|
||||
pub fn new(app_state: Arc<AppState>) -> Arc<Self> {
|
||||
let mut server = Self {
|
||||
peer: Peer::new(),
|
||||
app_state,
|
||||
store: Default::default(),
|
||||
handlers: Default::default(),
|
||||
notifications,
|
||||
};
|
||||
|
||||
server
|
||||
@@ -158,9 +144,7 @@ impl Server {
|
||||
.add_request_handler(Server::join_project)
|
||||
.add_message_handler(Server::leave_project)
|
||||
.add_message_handler(Server::update_project)
|
||||
.add_message_handler(Server::register_project_activity)
|
||||
.add_request_handler(Server::update_worktree)
|
||||
.add_message_handler(Server::update_worktree_extensions)
|
||||
.add_message_handler(Server::start_language_server)
|
||||
.add_message_handler(Server::update_language_server)
|
||||
.add_message_handler(Server::update_diagnostic_summary)
|
||||
@@ -194,19 +178,14 @@ impl Server {
|
||||
.add_message_handler(Server::buffer_reloaded)
|
||||
.add_message_handler(Server::buffer_saved)
|
||||
.add_request_handler(Server::save_buffer)
|
||||
.add_request_handler(Server::get_channels)
|
||||
.add_request_handler(Server::get_users)
|
||||
.add_request_handler(Server::fuzzy_search_users)
|
||||
.add_request_handler(Server::request_contact)
|
||||
.add_request_handler(Server::remove_contact)
|
||||
.add_request_handler(Server::respond_to_contact_request)
|
||||
.add_request_handler(Server::join_channel)
|
||||
.add_message_handler(Server::leave_channel)
|
||||
.add_request_handler(Server::send_channel_message)
|
||||
.add_request_handler(Server::follow)
|
||||
.add_message_handler(Server::unfollow)
|
||||
.add_message_handler(Server::update_followers)
|
||||
.add_request_handler(Server::get_channel_messages)
|
||||
.add_message_handler(Server::update_diff_base)
|
||||
.add_request_handler(Server::get_private_user_info);
|
||||
|
||||
@@ -290,58 +269,6 @@ impl Server {
|
||||
})
|
||||
}
|
||||
|
||||
/// Start a long lived task that records which users are active in which projects.
|
||||
pub fn start_recording_project_activity<E: 'static + Executor>(
|
||||
self: &Arc<Self>,
|
||||
interval: Duration,
|
||||
executor: E,
|
||||
) {
|
||||
executor.spawn_detached({
|
||||
let this = Arc::downgrade(self);
|
||||
let executor = executor.clone();
|
||||
async move {
|
||||
let mut period_start = OffsetDateTime::now_utc();
|
||||
let mut active_projects = Vec::<(UserId, ProjectId)>::new();
|
||||
loop {
|
||||
let sleep = executor.sleep(interval);
|
||||
sleep.await;
|
||||
let this = if let Some(this) = this.upgrade() {
|
||||
this
|
||||
} else {
|
||||
break;
|
||||
};
|
||||
|
||||
active_projects.clear();
|
||||
active_projects.extend(this.store().await.projects().flat_map(
|
||||
|(project_id, project)| {
|
||||
project.guests.values().chain([&project.host]).filter_map(
|
||||
|collaborator| {
|
||||
if !collaborator.admin
|
||||
&& collaborator
|
||||
.last_activity
|
||||
.map_or(false, |activity| activity > period_start)
|
||||
{
|
||||
Some((collaborator.user_id, *project_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
));
|
||||
|
||||
let period_end = OffsetDateTime::now_utc();
|
||||
this.app_state
|
||||
.db
|
||||
.record_user_activity(period_start..period_end, &active_projects)
|
||||
.await
|
||||
.trace_err();
|
||||
period_start = period_end;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn handle_connection<E: Executor>(
|
||||
self: &Arc<Self>,
|
||||
connection: Connection,
|
||||
@@ -432,18 +359,11 @@ impl Server {
|
||||
let span = tracing::info_span!("receive message", %user_id, %login, %connection_id, %address, type_name);
|
||||
let span_enter = span.enter();
|
||||
if let Some(handler) = this.handlers.get(&message.payload_type_id()) {
|
||||
let notifications = this.notifications.clone();
|
||||
let is_background = message.is_background();
|
||||
let handle_message = (handler)(this.clone(), message);
|
||||
|
||||
drop(span_enter);
|
||||
let handle_message = async move {
|
||||
handle_message.await;
|
||||
if let Some(mut notifications) = notifications {
|
||||
let _ = notifications.send(()).await;
|
||||
}
|
||||
}.instrument(span);
|
||||
|
||||
let handle_message = handle_message.instrument(span);
|
||||
if is_background {
|
||||
executor.spawn_detached(handle_message);
|
||||
} else {
|
||||
@@ -1024,7 +944,7 @@ impl Server {
|
||||
id: *id,
|
||||
root_name: worktree.root_name.clone(),
|
||||
visible: worktree.visible,
|
||||
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -1075,7 +995,7 @@ impl Server {
|
||||
let message = proto::UpdateWorktree {
|
||||
project_id: project_id.to_proto(),
|
||||
worktree_id: *worktree_id,
|
||||
abs_path: worktree.abs_path.as_os_str().as_bytes().to_vec(),
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
root_name: worktree.root_name.clone(),
|
||||
updated_entries: worktree.entries.values().cloned().collect(),
|
||||
removed_entries: Default::default(),
|
||||
@@ -1172,17 +1092,6 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_project_activity(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::RegisterProjectActivity>,
|
||||
) -> Result<()> {
|
||||
self.store().await.register_project_activity(
|
||||
ProjectId::from_proto(request.payload.project_id),
|
||||
request.sender_id,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_worktree(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateWorktree>,
|
||||
@@ -1195,6 +1104,7 @@ impl Server {
|
||||
project_id,
|
||||
worktree_id,
|
||||
&request.payload.root_name,
|
||||
&request.payload.abs_path,
|
||||
&request.payload.removed_entries,
|
||||
&request.payload.updated_entries,
|
||||
request.payload.scan_id,
|
||||
@@ -1209,25 +1119,6 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_worktree_extensions(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateWorktreeExtensions>,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let worktree_id = request.payload.worktree_id;
|
||||
let extensions = request
|
||||
.payload
|
||||
.extensions
|
||||
.into_iter()
|
||||
.zip(request.payload.counts)
|
||||
.collect();
|
||||
self.app_state
|
||||
.db
|
||||
.update_worktree_extensions(project_id, worktree_id, extensions)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diagnostic_summary(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateDiagnosticSummary>,
|
||||
@@ -1363,8 +1254,7 @@ impl Server {
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let receiver_ids = {
|
||||
let mut store = self.store().await;
|
||||
store.register_project_activity(project_id, request.sender_id)?;
|
||||
let store = self.store().await;
|
||||
store.project_connection_ids(project_id, request.sender_id)?
|
||||
};
|
||||
|
||||
@@ -1430,15 +1320,13 @@ impl Server {
|
||||
let leader_id = ConnectionId(request.payload.leader_id);
|
||||
let follower_id = request.sender_id;
|
||||
{
|
||||
let mut store = self.store().await;
|
||||
let store = self.store().await;
|
||||
if !store
|
||||
.project_connection_ids(project_id, follower_id)?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
|
||||
store.register_project_activity(project_id, follower_id)?;
|
||||
}
|
||||
|
||||
let mut response_payload = self
|
||||
@@ -1455,14 +1343,13 @@ impl Server {
|
||||
async fn unfollow(self: Arc<Self>, request: TypedEnvelope<proto::Unfollow>) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let leader_id = ConnectionId(request.payload.leader_id);
|
||||
let mut store = self.store().await;
|
||||
let store = self.store().await;
|
||||
if !store
|
||||
.project_connection_ids(project_id, request.sender_id)?
|
||||
.contains(&leader_id)
|
||||
{
|
||||
Err(anyhow!("no such peer"))?;
|
||||
}
|
||||
store.register_project_activity(project_id, request.sender_id)?;
|
||||
self.peer
|
||||
.forward_send(request.sender_id, leader_id, request.payload)?;
|
||||
Ok(())
|
||||
@@ -1473,8 +1360,7 @@ impl Server {
|
||||
request: TypedEnvelope<proto::UpdateFollowers>,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.payload.project_id);
|
||||
let mut store = self.store().await;
|
||||
store.register_project_activity(project_id, request.sender_id)?;
|
||||
let store = self.store().await;
|
||||
let connection_ids = store.project_connection_ids(project_id, request.sender_id)?;
|
||||
let leader_id = request
|
||||
.payload
|
||||
@@ -1495,28 +1381,6 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_channels(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::GetChannels>,
|
||||
response: Response<proto::GetChannels>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channels = self.app_state.db.get_accessible_channels(user_id).await?;
|
||||
response.send(proto::GetChannelsResponse {
|
||||
channels: channels
|
||||
.into_iter()
|
||||
.map(|chan| proto::Channel {
|
||||
id: chan.id.to_proto(),
|
||||
name: chan.name,
|
||||
})
|
||||
.collect(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_users(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::GetUsers>,
|
||||
@@ -1712,175 +1576,6 @@ impl Server {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn join_channel(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::JoinChannel>,
|
||||
response: Response<proto::JoinChannel>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
if !self
|
||||
.app_state
|
||||
.db
|
||||
.can_user_access_channel(user_id, channel_id)
|
||||
.await?
|
||||
{
|
||||
Err(anyhow!("access denied"))?;
|
||||
}
|
||||
|
||||
self.store()
|
||||
.await
|
||||
.join_channel(request.sender_id, channel_id);
|
||||
let messages = self
|
||||
.app_state
|
||||
.db
|
||||
.get_channel_messages(channel_id, MESSAGE_COUNT_PER_PAGE, None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|msg| proto::ChannelMessage {
|
||||
id: msg.id.to_proto(),
|
||||
body: msg.body,
|
||||
timestamp: msg.sent_at.unix_timestamp() as u64,
|
||||
sender_id: msg.sender_id.to_proto(),
|
||||
nonce: Some(msg.nonce.as_u128().into()),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
response.send(proto::JoinChannelResponse {
|
||||
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
|
||||
messages,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn leave_channel(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::LeaveChannel>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
if !self
|
||||
.app_state
|
||||
.db
|
||||
.can_user_access_channel(user_id, channel_id)
|
||||
.await?
|
||||
{
|
||||
Err(anyhow!("access denied"))?;
|
||||
}
|
||||
|
||||
self.store()
|
||||
.await
|
||||
.leave_channel(request.sender_id, channel_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_channel_message(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::SendChannelMessage>,
|
||||
response: Response<proto::SendChannelMessage>,
|
||||
) -> Result<()> {
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
let user_id;
|
||||
let connection_ids;
|
||||
{
|
||||
let state = self.store().await;
|
||||
user_id = state.user_id_for_connection(request.sender_id)?;
|
||||
connection_ids = state.channel_connection_ids(channel_id)?;
|
||||
}
|
||||
|
||||
// Validate the message body.
|
||||
let body = request.payload.body.trim().to_string();
|
||||
if body.len() > MAX_MESSAGE_LEN {
|
||||
return Err(anyhow!("message is too long"))?;
|
||||
}
|
||||
if body.is_empty() {
|
||||
return Err(anyhow!("message can't be blank"))?;
|
||||
}
|
||||
|
||||
let timestamp = OffsetDateTime::now_utc();
|
||||
let nonce = request
|
||||
.payload
|
||||
.nonce
|
||||
.ok_or_else(|| anyhow!("nonce can't be blank"))?;
|
||||
|
||||
let message_id = self
|
||||
.app_state
|
||||
.db
|
||||
.create_channel_message(channel_id, user_id, &body, timestamp, nonce.clone().into())
|
||||
.await?
|
||||
.to_proto();
|
||||
let message = proto::ChannelMessage {
|
||||
sender_id: user_id.to_proto(),
|
||||
id: message_id,
|
||||
body,
|
||||
timestamp: timestamp.unix_timestamp() as u64,
|
||||
nonce: Some(nonce),
|
||||
};
|
||||
broadcast(request.sender_id, connection_ids, |conn_id| {
|
||||
self.peer.send(
|
||||
conn_id,
|
||||
proto::ChannelMessageSent {
|
||||
channel_id: channel_id.to_proto(),
|
||||
message: Some(message.clone()),
|
||||
},
|
||||
)
|
||||
});
|
||||
response.send(proto::SendChannelMessageResponse {
|
||||
message: Some(message),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_channel_messages(
|
||||
self: Arc<Self>,
|
||||
request: TypedEnvelope<proto::GetChannelMessages>,
|
||||
response: Response<proto::GetChannelMessages>,
|
||||
) -> Result<()> {
|
||||
let user_id = self
|
||||
.store()
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let channel_id = ChannelId::from_proto(request.payload.channel_id);
|
||||
if !self
|
||||
.app_state
|
||||
.db
|
||||
.can_user_access_channel(user_id, channel_id)
|
||||
.await?
|
||||
{
|
||||
Err(anyhow!("access denied"))?;
|
||||
}
|
||||
|
||||
let messages = self
|
||||
.app_state
|
||||
.db
|
||||
.get_channel_messages(
|
||||
channel_id,
|
||||
MESSAGE_COUNT_PER_PAGE,
|
||||
Some(MessageId::from_proto(request.payload.before_message_id)),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|msg| proto::ChannelMessage {
|
||||
id: msg.id.to_proto(),
|
||||
body: msg.body,
|
||||
timestamp: msg.sent_at.unix_timestamp() as u64,
|
||||
sender_id: msg.sender_id.to_proto(),
|
||||
nonce: Some(msg.nonce.as_u128().into()),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
response.send(proto::GetChannelMessagesResponse {
|
||||
done: messages.len() < MESSAGE_COUNT_PER_PAGE,
|
||||
messages,
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_diff_base(
|
||||
self: Arc<Server>,
|
||||
request: TypedEnvelope<proto::UpdateDiffBase>,
|
||||
@@ -2061,11 +1756,8 @@ pub async fn handle_websocket_request(
|
||||
}
|
||||
|
||||
pub async fn handle_metrics(Extension(server): Extension<Arc<Server>>) -> axum::response::Response {
|
||||
// We call `store_mut` here for its side effects of updating metrics.
|
||||
let metrics = server.store().await.metrics();
|
||||
METRIC_CONNECTIONS.set(metrics.connections as _);
|
||||
METRIC_REGISTERED_PROJECTS.set(metrics.registered_projects as _);
|
||||
METRIC_ACTIVE_PROJECTS.set(metrics.active_projects as _);
|
||||
METRIC_SHARED_PROJECTS.set(metrics.shared_projects as _);
|
||||
|
||||
let encoder = prometheus::TextEncoder::new();
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::db::{self, ChannelId, ProjectId, UserId};
|
||||
use crate::db::{self, ProjectId, UserId};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::{btree_map, BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use nanoid::nanoid;
|
||||
use rpc::{proto, ConnectionId};
|
||||
use serde::Serialize;
|
||||
use std::{borrow::Cow, mem, path::PathBuf, str, time::Duration};
|
||||
use time::OffsetDateTime;
|
||||
use std::{borrow::Cow, mem, path::PathBuf, str};
|
||||
use tracing::instrument;
|
||||
use util::post_inc;
|
||||
|
||||
@@ -18,8 +17,6 @@ pub struct Store {
|
||||
next_room_id: RoomId,
|
||||
rooms: BTreeMap<RoomId, proto::Room>,
|
||||
projects: BTreeMap<ProjectId, Project>,
|
||||
#[serde(skip)]
|
||||
channels: BTreeMap<ChannelId, Channel>,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
@@ -33,7 +30,6 @@ struct ConnectionState {
|
||||
user_id: UserId,
|
||||
admin: bool,
|
||||
projects: BTreeSet<ProjectId>,
|
||||
channels: HashSet<ChannelId>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Serialize)]
|
||||
@@ -60,14 +56,12 @@ pub struct Project {
|
||||
pub struct Collaborator {
|
||||
pub replica_id: ReplicaId,
|
||||
pub user_id: UserId,
|
||||
#[serde(skip)]
|
||||
pub last_activity: Option<OffsetDateTime>,
|
||||
pub admin: bool,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
pub struct Worktree {
|
||||
pub abs_path: PathBuf,
|
||||
pub abs_path: Vec<u8>,
|
||||
pub root_name: String,
|
||||
pub visible: bool,
|
||||
#[serde(skip)]
|
||||
@@ -78,11 +72,6 @@ pub struct Worktree {
|
||||
pub is_complete: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Channel {
|
||||
pub connection_ids: HashSet<ConnectionId>,
|
||||
}
|
||||
|
||||
pub type ReplicaId = u16;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -113,38 +102,23 @@ pub struct LeftRoom<'a> {
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Metrics {
|
||||
pub connections: usize,
|
||||
pub registered_projects: usize,
|
||||
pub active_projects: usize,
|
||||
pub shared_projects: usize,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn metrics(&self) -> Metrics {
|
||||
const ACTIVE_PROJECT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
let active_window_start = OffsetDateTime::now_utc() - ACTIVE_PROJECT_TIMEOUT;
|
||||
|
||||
let connections = self.connections.values().filter(|c| !c.admin).count();
|
||||
let mut registered_projects = 0;
|
||||
let mut active_projects = 0;
|
||||
let mut shared_projects = 0;
|
||||
for project in self.projects.values() {
|
||||
if let Some(connection) = self.connections.get(&project.host_connection_id) {
|
||||
if !connection.admin {
|
||||
registered_projects += 1;
|
||||
if project.is_active_since(active_window_start) {
|
||||
active_projects += 1;
|
||||
if !project.guests.is_empty() {
|
||||
shared_projects += 1;
|
||||
}
|
||||
}
|
||||
shared_projects += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Metrics {
|
||||
connections,
|
||||
registered_projects,
|
||||
active_projects,
|
||||
shared_projects,
|
||||
}
|
||||
}
|
||||
@@ -162,7 +136,6 @@ impl Store {
|
||||
user_id,
|
||||
admin,
|
||||
projects: Default::default(),
|
||||
channels: Default::default(),
|
||||
},
|
||||
);
|
||||
let connected_user = self.connected_users.entry(user_id).or_default();
|
||||
@@ -201,18 +174,12 @@ impl Store {
|
||||
.ok_or_else(|| anyhow!("no such connection"))?;
|
||||
|
||||
let user_id = connection.user_id;
|
||||
let connection_channels = mem::take(&mut connection.channels);
|
||||
|
||||
let mut result = RemovedConnectionState {
|
||||
user_id,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Leave all channels.
|
||||
for channel_id in connection_channels {
|
||||
self.leave_channel(connection_id, channel_id);
|
||||
}
|
||||
|
||||
let connected_user = self.connected_users.get(&user_id).unwrap();
|
||||
if let Some(active_call) = connected_user.active_call.as_ref() {
|
||||
let room_id = active_call.room_id;
|
||||
@@ -238,34 +205,6 @@ impl Store {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn channel(&self, id: ChannelId) -> Option<&Channel> {
|
||||
self.channels.get(&id)
|
||||
}
|
||||
|
||||
pub fn join_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
|
||||
if let Some(connection) = self.connections.get_mut(&connection_id) {
|
||||
connection.channels.insert(channel_id);
|
||||
self.channels
|
||||
.entry(channel_id)
|
||||
.or_default()
|
||||
.connection_ids
|
||||
.insert(connection_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn leave_channel(&mut self, connection_id: ConnectionId, channel_id: ChannelId) {
|
||||
if let Some(connection) = self.connections.get_mut(&connection_id) {
|
||||
connection.channels.remove(&channel_id);
|
||||
if let btree_map::Entry::Occupied(mut entry) = self.channels.entry(channel_id) {
|
||||
entry.get_mut().connection_ids.remove(&connection_id);
|
||||
if entry.get_mut().connection_ids.is_empty() {
|
||||
entry.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_id_for_connection(&self, connection_id: ConnectionId) -> Result<UserId> {
|
||||
Ok(self
|
||||
.connections
|
||||
@@ -760,7 +699,6 @@ impl Store {
|
||||
host: Collaborator {
|
||||
user_id: connection.user_id,
|
||||
replica_id: 0,
|
||||
last_activity: None,
|
||||
admin: connection.admin,
|
||||
},
|
||||
guests: Default::default(),
|
||||
@@ -773,7 +711,11 @@ impl Store {
|
||||
Worktree {
|
||||
root_name: worktree.root_name,
|
||||
visible: worktree.visible,
|
||||
..Default::default()
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: Default::default(),
|
||||
is_complete: Default::default(),
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -852,7 +794,11 @@ impl Store {
|
||||
Worktree {
|
||||
root_name: worktree.root_name.clone(),
|
||||
visible: worktree.visible,
|
||||
..Default::default()
|
||||
abs_path: worktree.abs_path.clone(),
|
||||
entries: Default::default(),
|
||||
diagnostic_summaries: Default::default(),
|
||||
scan_id: Default::default(),
|
||||
is_complete: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -959,12 +905,10 @@ impl Store {
|
||||
Collaborator {
|
||||
replica_id,
|
||||
user_id: connection.user_id,
|
||||
last_activity: Some(OffsetDateTime::now_utc()),
|
||||
admin: connection.admin,
|
||||
},
|
||||
);
|
||||
|
||||
project.host.last_activity = Some(OffsetDateTime::now_utc());
|
||||
Ok((project, replica_id))
|
||||
}
|
||||
|
||||
@@ -1006,6 +950,7 @@ impl Store {
|
||||
project_id: ProjectId,
|
||||
worktree_id: u64,
|
||||
worktree_root_name: &str,
|
||||
worktree_abs_path: &[u8],
|
||||
removed_entries: &[u64],
|
||||
updated_entries: &[proto::Entry],
|
||||
scan_id: u64,
|
||||
@@ -1016,6 +961,7 @@ impl Store {
|
||||
let connection_ids = project.connection_ids();
|
||||
let mut worktree = project.worktrees.entry(worktree_id).or_default();
|
||||
worktree.root_name = worktree_root_name.to_string();
|
||||
worktree.abs_path = worktree_abs_path.to_vec();
|
||||
|
||||
for entry_id in removed_entries {
|
||||
worktree.entries.remove(entry_id);
|
||||
@@ -1056,44 +1002,12 @@ impl Store {
|
||||
.connection_ids())
|
||||
}
|
||||
|
||||
pub fn channel_connection_ids(&self, channel_id: ChannelId) -> Result<Vec<ConnectionId>> {
|
||||
Ok(self
|
||||
.channels
|
||||
.get(&channel_id)
|
||||
.ok_or_else(|| anyhow!("no such channel"))?
|
||||
.connection_ids())
|
||||
}
|
||||
|
||||
pub fn project(&self, project_id: ProjectId) -> Result<&Project> {
|
||||
self.projects
|
||||
.get(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))
|
||||
}
|
||||
|
||||
pub fn register_project_activity(
|
||||
&mut self,
|
||||
project_id: ProjectId,
|
||||
connection_id: ConnectionId,
|
||||
) -> Result<()> {
|
||||
let project = self
|
||||
.projects
|
||||
.get_mut(&project_id)
|
||||
.ok_or_else(|| anyhow!("no such project"))?;
|
||||
let collaborator = if connection_id == project.host_connection_id {
|
||||
&mut project.host
|
||||
} else if let Some(guest) = project.guests.get_mut(&connection_id) {
|
||||
guest
|
||||
} else {
|
||||
return Err(anyhow!("no such project"))?;
|
||||
};
|
||||
collaborator.last_activity = Some(OffsetDateTime::now_utc());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn projects(&self) -> impl Iterator<Item = (&ProjectId, &Project)> {
|
||||
self.projects.iter()
|
||||
}
|
||||
|
||||
pub fn read_project(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
@@ -1154,10 +1068,7 @@ impl Store {
|
||||
}
|
||||
}
|
||||
}
|
||||
for channel_id in &connection.channels {
|
||||
let channel = self.channels.get(channel_id).unwrap();
|
||||
assert!(channel.connection_ids.contains(connection_id));
|
||||
}
|
||||
|
||||
assert!(self
|
||||
.connected_users
|
||||
.get(&connection.user_id)
|
||||
@@ -1253,28 +1164,10 @@ impl Store {
|
||||
"project was not shared in room"
|
||||
);
|
||||
}
|
||||
|
||||
for (channel_id, channel) in &self.channels {
|
||||
for connection_id in &channel.connection_ids {
|
||||
let connection = self.connections.get(connection_id).unwrap();
|
||||
assert!(connection.channels.contains(channel_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
fn is_active_since(&self, start_time: OffsetDateTime) -> bool {
|
||||
self.guests
|
||||
.values()
|
||||
.chain([&self.host])
|
||||
.any(|collaborator| {
|
||||
collaborator
|
||||
.last_activity
|
||||
.map_or(false, |active_time| active_time > start_time)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn guest_connection_ids(&self) -> Vec<ConnectionId> {
|
||||
self.guests.keys().copied().collect()
|
||||
}
|
||||
@@ -1287,9 +1180,3 @@ impl Project {
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Channel {
|
||||
fn connection_ids(&self) -> Vec<ConnectionId> {
|
||||
self.connection_ids.iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,7 +452,7 @@ impl ProjectDiagnosticsEditor {
|
||||
} else {
|
||||
groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice();
|
||||
new_excerpt_ids_by_selection_id =
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| s.refresh());
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh());
|
||||
selections = editor.selections.all::<usize>(cx);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@ collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
||||
@@ -2,29 +2,55 @@ use std::{any::Any, rc::Rc};
|
||||
|
||||
use collections::HashSet;
|
||||
use gpui::{
|
||||
elements::{MouseEventHandler, Overlay},
|
||||
geometry::vector::Vector2F,
|
||||
scene::MouseDrag,
|
||||
elements::{Empty, MouseEventHandler, Overlay},
|
||||
geometry::{rect::RectF, vector::Vector2F},
|
||||
scene::{MouseDown, MouseDrag},
|
||||
CursorStyle, Element, ElementBox, EventContext, MouseButton, MutableAppContext, RenderContext,
|
||||
View, WeakViewHandle,
|
||||
};
|
||||
|
||||
struct State<V: View> {
|
||||
window_id: usize,
|
||||
position: Vector2F,
|
||||
region_offset: Vector2F,
|
||||
payload: Rc<dyn Any + 'static>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
|
||||
enum State<V: View> {
|
||||
Down {
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
},
|
||||
Dragging {
|
||||
window_id: usize,
|
||||
position: Vector2F,
|
||||
region_offset: Vector2F,
|
||||
region: RectF,
|
||||
payload: Rc<dyn Any + 'static>,
|
||||
render: Rc<dyn Fn(Rc<dyn Any>, &mut RenderContext<V>) -> ElementBox>,
|
||||
},
|
||||
Canceled,
|
||||
}
|
||||
|
||||
impl<V: View> Clone for State<V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
window_id: self.window_id.clone(),
|
||||
position: self.position.clone(),
|
||||
region_offset: self.region_offset.clone(),
|
||||
payload: self.payload.clone(),
|
||||
render: self.render.clone(),
|
||||
match self {
|
||||
&State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
} => State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
},
|
||||
State::Dragging {
|
||||
window_id,
|
||||
position,
|
||||
region_offset,
|
||||
region,
|
||||
payload,
|
||||
render,
|
||||
} => Self::Dragging {
|
||||
window_id: window_id.clone(),
|
||||
position: position.clone(),
|
||||
region_offset: region_offset.clone(),
|
||||
region: region.clone(),
|
||||
payload: payload.clone(),
|
||||
render: render.clone(),
|
||||
},
|
||||
State::Canceled => State::Canceled,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,24 +75,36 @@ impl<V: View> DragAndDrop<V> {
|
||||
}
|
||||
|
||||
pub fn currently_dragged<T: Any>(&self, window_id: usize) -> Option<(Vector2F, Rc<T>)> {
|
||||
self.currently_dragged.as_ref().and_then(
|
||||
|State {
|
||||
position,
|
||||
payload,
|
||||
window_id: window_dragged_from,
|
||||
..
|
||||
}| {
|
||||
self.currently_dragged.as_ref().and_then(|state| {
|
||||
if let State::Dragging {
|
||||
position,
|
||||
payload,
|
||||
window_id: window_dragged_from,
|
||||
..
|
||||
} = state
|
||||
{
|
||||
if &window_id != window_dragged_from {
|
||||
return None;
|
||||
}
|
||||
|
||||
payload
|
||||
.clone()
|
||||
.downcast::<T>()
|
||||
.ok()
|
||||
.is::<T>()
|
||||
.then(|| payload.clone().downcast::<T>().ok())
|
||||
.flatten()
|
||||
.map(|payload| (position.clone(), payload))
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn drag_started(event: MouseDown, cx: &mut EventContext) {
|
||||
cx.update_global(|this: &mut Self, _| {
|
||||
this.currently_dragged = Some(State::Down {
|
||||
region_offset: event.region.origin() - event.position,
|
||||
region: event.region,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dragging<T: Any>(
|
||||
@@ -76,75 +114,132 @@ impl<V: View> DragAndDrop<V> {
|
||||
render: Rc<impl 'static + Fn(&T, &mut RenderContext<V>) -> ElementBox>,
|
||||
) {
|
||||
let window_id = cx.window_id();
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
let region_offset = if let Some(previous_state) = this.currently_dragged.as_ref() {
|
||||
previous_state.region_offset
|
||||
} else {
|
||||
event.region.origin() - event.prev_mouse_position
|
||||
};
|
||||
|
||||
this.currently_dragged = Some(State {
|
||||
window_id,
|
||||
region_offset,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
|
||||
cx.update_global(|this: &mut Self, cx| {
|
||||
this.notify_containers_for_window(window_id, cx);
|
||||
|
||||
match this.currently_dragged.as_ref() {
|
||||
Some(&State::Down {
|
||||
region_offset,
|
||||
region,
|
||||
})
|
||||
| Some(&State::Dragging {
|
||||
region_offset,
|
||||
region,
|
||||
..
|
||||
}) => {
|
||||
this.currently_dragged = Some(State::Dragging {
|
||||
window_id,
|
||||
region_offset,
|
||||
region,
|
||||
position: event.position,
|
||||
payload,
|
||||
render: Rc::new(move |payload, cx| {
|
||||
render(payload.downcast_ref::<T>().unwrap(), cx)
|
||||
}),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn render(cx: &mut RenderContext<V>) -> Option<ElementBox> {
|
||||
let currently_dragged = cx.global::<Self>().currently_dragged.clone();
|
||||
enum DraggedElementHandler {}
|
||||
cx.global::<Self>()
|
||||
.currently_dragged
|
||||
.clone()
|
||||
.and_then(|state| {
|
||||
match state {
|
||||
State::Down { .. } => None,
|
||||
State::Dragging {
|
||||
window_id,
|
||||
region_offset,
|
||||
position,
|
||||
region,
|
||||
payload,
|
||||
render,
|
||||
} => {
|
||||
if cx.window_id() != window_id {
|
||||
return None;
|
||||
}
|
||||
|
||||
currently_dragged.and_then(
|
||||
|State {
|
||||
window_id,
|
||||
region_offset,
|
||||
position,
|
||||
payload,
|
||||
render,
|
||||
}| {
|
||||
if cx.window_id() != window_id {
|
||||
return None;
|
||||
}
|
||||
let position = position + region_offset;
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
});
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| {
|
||||
this.finish_dragging(cx)
|
||||
});
|
||||
});
|
||||
})
|
||||
// Don't block hover events or invalidations
|
||||
.with_hoverable(false)
|
||||
.constrained()
|
||||
.with_width(region.width())
|
||||
.with_height(region.height())
|
||||
.boxed(),
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
let position = position + region_offset;
|
||||
|
||||
enum DraggedElementHandler {}
|
||||
Some(
|
||||
Overlay::new(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, cx| {
|
||||
render(payload, cx)
|
||||
State::Canceled => Some(
|
||||
MouseEventHandler::<DraggedElementHandler>::new(0, cx, |_, _| {
|
||||
Empty::new()
|
||||
.constrained()
|
||||
.with_width(0.)
|
||||
.with_height(0.)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::Arrow)
|
||||
.on_up(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
});
|
||||
cx.propagate_event();
|
||||
})
|
||||
.on_up_out(MouseButton::Left, |_, cx| {
|
||||
cx.defer(|cx| {
|
||||
cx.update_global::<Self, _, _>(|this, cx| this.stop_dragging(cx));
|
||||
cx.update_global::<Self, _, _>(|this, _| {
|
||||
this.currently_dragged = None;
|
||||
});
|
||||
});
|
||||
})
|
||||
// Don't block hover events or invalidations
|
||||
.with_hoverable(false)
|
||||
.boxed(),
|
||||
)
|
||||
.with_anchor_position(position)
|
||||
.boxed(),
|
||||
)
|
||||
},
|
||||
)
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn stop_dragging(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State { window_id, .. }) = self.currently_dragged.take() {
|
||||
pub fn cancel_dragging<P: Any>(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State::Dragging {
|
||||
payload, window_id, ..
|
||||
}) = &self.currently_dragged
|
||||
{
|
||||
if payload.is::<P>() {
|
||||
let window_id = *window_id;
|
||||
self.currently_dragged = Some(State::Canceled);
|
||||
self.notify_containers_for_window(window_id, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_dragging(&mut self, cx: &mut MutableAppContext) {
|
||||
if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() {
|
||||
self.notify_containers_for_window(window_id, cx);
|
||||
}
|
||||
}
|
||||
@@ -184,7 +279,11 @@ impl<Tag> Draggable for MouseEventHandler<Tag> {
|
||||
{
|
||||
let payload = Rc::new(payload);
|
||||
let render = Rc::new(render);
|
||||
self.on_drag(MouseButton::Left, move |e, cx| {
|
||||
self.on_down(MouseButton::Left, move |e, cx| {
|
||||
cx.propagate_event();
|
||||
DragAndDrop::<V>::drag_started(e, cx);
|
||||
})
|
||||
.on_drag(MouseButton::Left, move |e, cx| {
|
||||
let payload = payload.clone();
|
||||
let render = render.clone();
|
||||
DragAndDrop::<V>::dragging(e, payload, cx, render)
|
||||
|
||||
@@ -187,7 +187,7 @@ actions!(
|
||||
Paste,
|
||||
Undo,
|
||||
Redo,
|
||||
CenterScreen,
|
||||
NextScreen,
|
||||
MoveUp,
|
||||
PageUp,
|
||||
MoveDown,
|
||||
@@ -307,7 +307,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::move_down);
|
||||
cx.add_action(Editor::move_page_down);
|
||||
cx.add_action(Editor::page_down);
|
||||
cx.add_action(Editor::center_screen);
|
||||
cx.add_action(Editor::next_screen);
|
||||
|
||||
cx.add_action(Editor::move_left);
|
||||
cx.add_action(Editor::move_right);
|
||||
cx.add_action(Editor::move_to_previous_word_start);
|
||||
@@ -409,9 +410,42 @@ pub enum SelectMode {
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
pub enum Autoscroll {
|
||||
Next,
|
||||
Strategy(AutoscrollStrategy),
|
||||
}
|
||||
|
||||
impl Autoscroll {
|
||||
pub fn fit() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Fit)
|
||||
}
|
||||
|
||||
pub fn newest() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Newest)
|
||||
}
|
||||
|
||||
pub fn center() -> Self {
|
||||
Self::Strategy(AutoscrollStrategy::Center)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Default)]
|
||||
pub enum AutoscrollStrategy {
|
||||
Fit,
|
||||
Center,
|
||||
Newest,
|
||||
#[default]
|
||||
Center,
|
||||
Top,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
impl AutoscrollStrategy {
|
||||
fn next(&self) -> Self {
|
||||
match self {
|
||||
AutoscrollStrategy::Center => AutoscrollStrategy::Top,
|
||||
AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
|
||||
_ => AutoscrollStrategy::Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
@@ -553,6 +587,7 @@ pub struct Editor {
|
||||
hover_state: HoverState,
|
||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||
visible_line_count: Option<f32>,
|
||||
last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -1205,6 +1240,7 @@ impl Editor {
|
||||
hover_state: Default::default(),
|
||||
link_go_to_definition_state: Default::default(),
|
||||
visible_line_count: None,
|
||||
last_autoscroll: None,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
cx.subscribe(&buffer, Self::on_buffer_event),
|
||||
@@ -1435,7 +1471,7 @@ impl Editor {
|
||||
if let Some(highlighted_rows) = &self.highlighted_rows {
|
||||
first_cursor_top = highlighted_rows.start as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
} else if autoscroll == Autoscroll::Newest {
|
||||
} else if autoscroll == Autoscroll::newest() {
|
||||
let newest_selection = self.selections.newest::<Point>(cx);
|
||||
first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
@@ -1465,8 +1501,27 @@ impl Editor {
|
||||
return false;
|
||||
}
|
||||
|
||||
match autoscroll {
|
||||
Autoscroll::Fit | Autoscroll::Newest => {
|
||||
let strategy = match autoscroll {
|
||||
Autoscroll::Strategy(strategy) => strategy,
|
||||
Autoscroll::Next => {
|
||||
let last_autoscroll = &self.last_autoscroll;
|
||||
if let Some(last_autoscroll) = last_autoscroll {
|
||||
if self.scroll_position == last_autoscroll.0
|
||||
&& first_cursor_top == last_autoscroll.1
|
||||
&& last_cursor_bottom == last_autoscroll.2
|
||||
{
|
||||
last_autoscroll.3.next()
|
||||
} else {
|
||||
AutoscrollStrategy::default()
|
||||
}
|
||||
} else {
|
||||
AutoscrollStrategy::default()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
let margin = margin.min(self.vertical_scroll_margin);
|
||||
let target_top = (first_cursor_top - margin).max(0.0);
|
||||
let target_bottom = last_cursor_bottom + margin;
|
||||
@@ -1481,12 +1536,27 @@ impl Editor {
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
}
|
||||
Autoscroll::Center => {
|
||||
AutoscrollStrategy::Center => {
|
||||
scroll_position.set_y((first_cursor_top - margin).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
AutoscrollStrategy::Top => {
|
||||
scroll_position.set_y((first_cursor_top).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
AutoscrollStrategy::Bottom => {
|
||||
scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
|
||||
self.set_scroll_position_internal(scroll_position, local, cx);
|
||||
}
|
||||
}
|
||||
|
||||
self.last_autoscroll = Some((
|
||||
self.scroll_position,
|
||||
first_cursor_top,
|
||||
last_cursor_bottom,
|
||||
strategy,
|
||||
));
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1734,7 +1804,7 @@ impl Editor {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.set_pending(pending_selection, pending_mode)
|
||||
});
|
||||
}
|
||||
@@ -1795,7 +1865,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.change_selections(auto_scroll.then(|| Autoscroll::Newest), cx, |s| {
|
||||
self.change_selections(auto_scroll.then(|| Autoscroll::newest()), cx, |s| {
|
||||
if !add {
|
||||
s.clear_disjoint();
|
||||
} else if click_count > 1 {
|
||||
@@ -2012,7 +2082,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.change_selections(Some(Autoscroll::Fit), cx, |s| s.try_cancel()) {
|
||||
if self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -2178,7 +2248,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
this.trigger_completion_on_input(&text, cx);
|
||||
});
|
||||
}
|
||||
@@ -2258,7 +2328,7 @@ impl Editor {
|
||||
})
|
||||
.collect();
|
||||
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2288,7 +2358,7 @@ impl Editor {
|
||||
self.transact(cx, |editor, cx| {
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let mut index = 0;
|
||||
s.move_cursors_with(|map, _, _| {
|
||||
let row = rows[index];
|
||||
@@ -2329,7 +2399,7 @@ impl Editor {
|
||||
anchors
|
||||
});
|
||||
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_anchors(selection_anchors);
|
||||
})
|
||||
});
|
||||
@@ -3025,7 +3095,7 @@ impl Editor {
|
||||
});
|
||||
|
||||
if let Some(tabstop) = tabstops.first() {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(tabstop.iter().cloned());
|
||||
});
|
||||
self.snippet_stack.push(SnippetState {
|
||||
@@ -3066,7 +3136,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_anchor_ranges(current_ranges.iter().cloned())
|
||||
});
|
||||
// If snippet state is not at the last tabstop, push it back on the stack
|
||||
@@ -3131,14 +3201,14 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.insert("", cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -3232,7 +3302,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections))
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3255,7 +3325,7 @@ impl Editor {
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3387,7 +3457,7 @@ impl Editor {
|
||||
);
|
||||
});
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3467,7 +3537,7 @@ impl Editor {
|
||||
})
|
||||
.collect();
|
||||
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(new_selections);
|
||||
});
|
||||
});
|
||||
@@ -3509,7 +3579,7 @@ impl Editor {
|
||||
buffer.edit(edits, None, cx);
|
||||
});
|
||||
|
||||
this.request_autoscroll(Autoscroll::Fit, cx);
|
||||
this.request_autoscroll(Autoscroll::fit(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3619,7 +3689,7 @@ impl Editor {
|
||||
}
|
||||
});
|
||||
this.fold_ranges(refold_ranges, cx);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(new_selections);
|
||||
})
|
||||
});
|
||||
@@ -3724,13 +3794,13 @@ impl Editor {
|
||||
}
|
||||
});
|
||||
this.fold_ranges(refold_ranges, cx);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(new_selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
|
||||
});
|
||||
}
|
||||
|
||||
pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
let edits = this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let mut edits: Vec<(Range<usize>, String)> = Default::default();
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|display_map, selection| {
|
||||
@@ -3774,7 +3844,7 @@ impl Editor {
|
||||
this.buffer
|
||||
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
});
|
||||
@@ -3808,7 +3878,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
this.insert("", cx);
|
||||
@@ -3923,7 +3993,7 @@ impl Editor {
|
||||
});
|
||||
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
} else {
|
||||
this.insert(&clipboard_text, cx);
|
||||
}
|
||||
@@ -3938,7 +4008,7 @@ impl Editor {
|
||||
s.select_anchors(selections.to_vec());
|
||||
});
|
||||
}
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.unmark_text(cx);
|
||||
cx.emit(Event::Edited);
|
||||
}
|
||||
@@ -3952,7 +4022,7 @@ impl Editor {
|
||||
s.select_anchors(selections.to_vec());
|
||||
});
|
||||
}
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
self.unmark_text(cx);
|
||||
cx.emit(Event::Edited);
|
||||
}
|
||||
@@ -3964,7 +4034,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
let cursor = if selection.is_empty() && !line_mode {
|
||||
@@ -3978,13 +4048,13 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
let cursor = if selection.is_empty() && !line_mode {
|
||||
@@ -3998,12 +4068,12 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
|
||||
})
|
||||
}
|
||||
|
||||
pub fn center_screen(&mut self, _: &CenterScreen, cx: &mut ViewContext<Self>) {
|
||||
pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) {
|
||||
if self.take_rename(true, cx).is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -4017,7 +4087,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.request_autoscroll(Autoscroll::Center, cx);
|
||||
self.request_autoscroll(Autoscroll::Next, cx);
|
||||
}
|
||||
|
||||
pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
|
||||
@@ -4036,7 +4106,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
@@ -4070,9 +4140,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
Autoscroll::Center
|
||||
Autoscroll::center()
|
||||
} else {
|
||||
Autoscroll::Fit
|
||||
Autoscroll::fit()
|
||||
};
|
||||
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
@@ -4115,7 +4185,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
|
||||
})
|
||||
}
|
||||
@@ -4134,7 +4204,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if !selection.is_empty() && !line_mode {
|
||||
@@ -4168,9 +4238,9 @@ impl Editor {
|
||||
};
|
||||
|
||||
let autoscroll = if action.center_cursor {
|
||||
Autoscroll::Center
|
||||
Autoscroll::center()
|
||||
} else {
|
||||
Autoscroll::Fit
|
||||
Autoscroll::fit()
|
||||
};
|
||||
|
||||
self.change_selections(Some(autoscroll), cx, |s| {
|
||||
@@ -4213,7 +4283,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
|
||||
});
|
||||
}
|
||||
@@ -4223,7 +4293,7 @@ impl Editor {
|
||||
_: &MoveToPreviousWordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_word_start(map, head),
|
||||
@@ -4238,7 +4308,7 @@ impl Editor {
|
||||
_: &MoveToPreviousSubwordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_subword_start(map, head),
|
||||
@@ -4253,7 +4323,7 @@ impl Editor {
|
||||
_: &SelectToPreviousWordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_word_start(map, head),
|
||||
@@ -4268,7 +4338,7 @@ impl Editor {
|
||||
_: &SelectToPreviousSubwordStart,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::previous_subword_start(map, head),
|
||||
@@ -4285,7 +4355,7 @@ impl Editor {
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.select_autoclose_pair(cx);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -4305,7 +4375,7 @@ impl Editor {
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.select_autoclose_pair(cx);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -4319,7 +4389,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(movement::next_word_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4331,7 +4401,7 @@ impl Editor {
|
||||
_: &MoveToNextSubwordEnd,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(movement::next_subword_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4339,7 +4409,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::next_word_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4351,7 +4421,7 @@ impl Editor {
|
||||
_: &SelectToNextSubwordEnd,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::next_subword_end(map, head), SelectionGoal::None)
|
||||
});
|
||||
@@ -4360,7 +4430,7 @@ impl Editor {
|
||||
|
||||
pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() && !line_mode {
|
||||
@@ -4379,7 +4449,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.is_empty() {
|
||||
let cursor = movement::next_subword_end(map, selection.head());
|
||||
@@ -4396,7 +4466,7 @@ impl Editor {
|
||||
_: &MoveToBeginningOfLine,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(
|
||||
movement::indented_line_beginning(map, head, true),
|
||||
@@ -4411,7 +4481,7 @@ impl Editor {
|
||||
action: &SelectToBeginningOfLine,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
|
||||
@@ -4427,7 +4497,7 @@ impl Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.transact(cx, |this, cx| {
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|_, selection| {
|
||||
selection.reversed = true;
|
||||
});
|
||||
@@ -4444,7 +4514,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext<Self>) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, head, _| {
|
||||
(movement::line_end(map, head, true), SelectionGoal::None)
|
||||
});
|
||||
@@ -4456,7 +4526,7 @@ impl Editor {
|
||||
action: &SelectToEndOfLine,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(
|
||||
movement::line_end(map, head, action.stop_at_soft_wraps),
|
||||
@@ -4496,7 +4566,7 @@ impl Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(vec![0..0]);
|
||||
});
|
||||
}
|
||||
@@ -4505,7 +4575,7 @@ impl Editor {
|
||||
let mut selection = self.selections.last::<Point>(cx);
|
||||
selection.set_head(Point::zero(), SelectionGoal::None);
|
||||
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(vec![selection]);
|
||||
});
|
||||
}
|
||||
@@ -4517,7 +4587,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let cursor = self.buffer.read(cx).read(cx).len();
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(vec![cursor..cursor])
|
||||
});
|
||||
}
|
||||
@@ -4566,14 +4636,14 @@ impl Editor {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let mut selection = self.selections.first::<usize>(cx);
|
||||
selection.set_head(buffer.len(), SelectionGoal::None);
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(vec![selection]);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
|
||||
let end = self.buffer.read(cx).read(cx).len();
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(vec![0..end]);
|
||||
});
|
||||
}
|
||||
@@ -4588,7 +4658,7 @@ impl Editor {
|
||||
selection.end = cmp::min(max_point, Point::new(rows.end, 0));
|
||||
selection.reversed = false;
|
||||
}
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
}
|
||||
@@ -4613,7 +4683,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
self.unfold_ranges(to_unfold, true, cx);
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(new_selection_ranges);
|
||||
});
|
||||
}
|
||||
@@ -4713,7 +4783,7 @@ impl Editor {
|
||||
state.stack.pop();
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(new_selections);
|
||||
});
|
||||
if state.stack.len() > 1 {
|
||||
@@ -4762,7 +4832,7 @@ impl Editor {
|
||||
|
||||
if let Some(next_selected_range) = next_selected_range {
|
||||
self.unfold_ranges([next_selected_range.clone()], false, cx);
|
||||
self.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
if action.replace_newest {
|
||||
s.delete(s.newest_anchor().id);
|
||||
}
|
||||
@@ -4795,7 +4865,7 @@ impl Editor {
|
||||
done: false,
|
||||
};
|
||||
self.unfold_ranges([selection.start..selection.end], false, cx);
|
||||
self.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
self.select_next_state = Some(select_state);
|
||||
@@ -5028,7 +5098,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
drop(snapshot);
|
||||
this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5072,7 +5142,7 @@ impl Editor {
|
||||
|
||||
if selected_larger_node {
|
||||
stack.push(old_selections);
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(new_selections);
|
||||
});
|
||||
}
|
||||
@@ -5086,7 +5156,7 @@ impl Editor {
|
||||
) {
|
||||
let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
|
||||
if let Some(selections) = stack.pop() {
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(selections.to_vec());
|
||||
});
|
||||
}
|
||||
@@ -5117,7 +5187,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(selections);
|
||||
});
|
||||
}
|
||||
@@ -5129,7 +5199,7 @@ impl Editor {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::Newest, cx);
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
@@ -5141,7 +5211,7 @@ impl Editor {
|
||||
self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
|
||||
self.select_next_state = entry.select_next_state;
|
||||
self.add_selections_state = entry.add_selections_state;
|
||||
self.request_autoscroll(Autoscroll::Newest, cx);
|
||||
self.request_autoscroll(Autoscroll::newest(), cx);
|
||||
}
|
||||
self.selection_history.mode = SelectionHistoryMode::Normal;
|
||||
}
|
||||
@@ -5163,7 +5233,7 @@ impl Editor {
|
||||
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
|
||||
let (group_id, jump_to) = popover.activation_info();
|
||||
if self.activate_diagnostics(group_id, cx) {
|
||||
self.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
let mut new_selection = s.newest_anchor().clone();
|
||||
new_selection.collapse_to(jump_to, SelectionGoal::None);
|
||||
s.select_anchors(vec![new_selection.clone()]);
|
||||
@@ -5209,7 +5279,7 @@ impl Editor {
|
||||
|
||||
if let Some((primary_range, group_id)) = group {
|
||||
if self.activate_diagnostics(group_id, cx) {
|
||||
self.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select(vec![Selection {
|
||||
id: selection.id,
|
||||
start: primary_range.start,
|
||||
@@ -5284,7 +5354,7 @@ impl Editor {
|
||||
.dedup();
|
||||
|
||||
if let Some(hunk) = hunks.next() {
|
||||
this.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
this.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
let row = hunk.start_display_row();
|
||||
let point = DisplayPoint::new(row, 0);
|
||||
s.select_display_ranges([point..point]);
|
||||
@@ -5382,7 +5452,7 @@ impl Editor {
|
||||
if editor_handle != target_editor_handle {
|
||||
pane.update(cx, |pane, _| pane.disable_history());
|
||||
}
|
||||
target_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
target_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([range]);
|
||||
});
|
||||
|
||||
@@ -6004,7 +6074,7 @@ impl Editor {
|
||||
let mut ranges = ranges.into_iter().peekable();
|
||||
if ranges.peek().is_some() {
|
||||
self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -6019,7 +6089,7 @@ impl Editor {
|
||||
if ranges.peek().is_some() {
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -6032,7 +6102,7 @@ impl Editor {
|
||||
let blocks = self
|
||||
.display_map
|
||||
.update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
blocks
|
||||
}
|
||||
|
||||
@@ -6043,7 +6113,7 @@ impl Editor {
|
||||
) {
|
||||
self.display_map
|
||||
.update(cx, |display_map, _| display_map.replace_blocks(blocks));
|
||||
self.request_autoscroll(Autoscroll::Fit, cx);
|
||||
self.request_autoscroll(Autoscroll::fit(), cx);
|
||||
}
|
||||
|
||||
pub fn remove_blocks(&mut self, block_ids: HashSet<BlockId>, cx: &mut ViewContext<Self>) {
|
||||
@@ -6383,7 +6453,7 @@ impl Editor {
|
||||
for (buffer, ranges) in new_selections_by_buffer.into_iter() {
|
||||
let editor = workspace.open_project_item::<Self>(buffer, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.select_ranges(ranges);
|
||||
});
|
||||
});
|
||||
@@ -6394,7 +6464,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn jump(workspace: &mut Workspace, action: &Jump, cx: &mut ViewContext<Workspace>) {
|
||||
let editor = workspace.open_path(action.path.clone(), true, cx);
|
||||
let editor = workspace.open_path(action.path.clone(), None, true, cx);
|
||||
let position = action.position;
|
||||
let anchor = action.anchor;
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
@@ -6409,7 +6479,7 @@ impl Editor {
|
||||
};
|
||||
|
||||
let nav_history = editor.nav_history.take();
|
||||
editor.change_selections(Some(Autoscroll::Newest), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
|
||||
s.select_ranges([cursor..cursor]);
|
||||
});
|
||||
editor.nav_history = nav_history;
|
||||
|
||||
@@ -4146,14 +4146,26 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
handle_resolve_completion_request(
|
||||
&mut cx,
|
||||
Some((
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"\nadditional edit",
|
||||
)),
|
||||
Some(vec![
|
||||
(
|
||||
//This overlaps with the primary completion edit which is
|
||||
//misbehavior from the LSP spec, test that we filter it out
|
||||
indoc! {"
|
||||
one.second_ˇcompletion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"overlapping aditional edit",
|
||||
),
|
||||
(
|
||||
indoc! {"
|
||||
one.second_completion
|
||||
two
|
||||
threeˇ
|
||||
"},
|
||||
"\nadditional edit",
|
||||
),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
apply_additional_edits.await.unwrap();
|
||||
@@ -4303,19 +4315,24 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
async fn handle_resolve_completion_request<'a>(
|
||||
cx: &mut EditorLspTestContext<'a>,
|
||||
edit: Option<(&'static str, &'static str)>,
|
||||
edits: Option<Vec<(&'static str, &'static str)>>,
|
||||
) {
|
||||
let edit = edit.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
vec![lsp::TextEdit::new(replace_range, new_text.to_string())]
|
||||
let edits = edits.map(|edits| {
|
||||
edits
|
||||
.iter()
|
||||
.map(|(marked_string, new_text)| {
|
||||
let (_, marked_ranges) = marked_text_ranges(marked_string, false);
|
||||
let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
|
||||
lsp::TextEdit::new(replace_range, new_text.to_string())
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
|
||||
let edit = edit.clone();
|
||||
let edits = edits.clone();
|
||||
async move {
|
||||
Ok(lsp::CompletionItem {
|
||||
additional_text_edits: edit,
|
||||
additional_text_edits: edits,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
@@ -5011,7 +5028,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
|
||||
// Update the selections and scroll position
|
||||
leader.update(cx, |leader, cx| {
|
||||
leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
|
||||
leader.request_autoscroll(Autoscroll::Newest, cx);
|
||||
leader.request_autoscroll(Autoscroll::newest(), cx);
|
||||
leader.set_scroll_position(vec2f(1.5, 3.5), cx);
|
||||
});
|
||||
follower.update(cx, |follower, cx| {
|
||||
|
||||
@@ -192,8 +192,14 @@ impl EditorElement {
|
||||
.on_scroll({
|
||||
let position_map = position_map.clone();
|
||||
move |e, cx| {
|
||||
if !Self::scroll(e.position, e.delta, e.precise, &position_map, bounds, cx)
|
||||
{
|
||||
if !Self::scroll(
|
||||
e.position,
|
||||
*e.delta.raw(),
|
||||
e.delta.precise(),
|
||||
&position_map,
|
||||
bounds,
|
||||
cx,
|
||||
) {
|
||||
cx.propagate_event()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +204,7 @@ impl FollowableItem for Editor {
|
||||
|
||||
if !selections.is_empty() {
|
||||
self.set_selections_from_remote(selections, cx);
|
||||
self.request_autoscroll_remotely(Autoscroll::Newest, cx);
|
||||
self.request_autoscroll_remotely(Autoscroll::newest(), cx);
|
||||
} else if let Some(anchor) = message.scroll_top_anchor {
|
||||
self.set_scroll_top_anchor(
|
||||
Anchor {
|
||||
@@ -294,7 +294,7 @@ impl Item for Editor {
|
||||
let nav_history = self.nav_history.take();
|
||||
self.scroll_position = data.scroll_position;
|
||||
self.scroll_top_anchor = scroll_top_anchor;
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([offset..offset])
|
||||
});
|
||||
self.nav_history = nav_history;
|
||||
@@ -466,7 +466,7 @@ impl Item for Editor {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let transaction = reload_buffers.log_err().await;
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
editor.request_autoscroll(Autoscroll::fit(), cx)
|
||||
});
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
if let Some(transaction) = transaction {
|
||||
@@ -619,7 +619,7 @@ impl SearchableItem for Editor {
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.unfold_ranges([matches[index].clone()], false, cx);
|
||||
self.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([matches[index].clone()])
|
||||
});
|
||||
}
|
||||
@@ -819,11 +819,20 @@ impl StatusItemView for CursorPosition {
|
||||
|
||||
fn path_for_buffer<'a>(
|
||||
buffer: &ModelHandle<MultiBuffer>,
|
||||
mut height: usize,
|
||||
height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
|
||||
path_for_file(file, height, include_filename, cx)
|
||||
}
|
||||
|
||||
fn path_for_file<'a>(
|
||||
file: &'a dyn language::File,
|
||||
mut height: usize,
|
||||
include_filename: bool,
|
||||
cx: &'a AppContext,
|
||||
) -> Option<Cow<'a, Path>> {
|
||||
// Ensure we always render at least the filename.
|
||||
height += 1;
|
||||
|
||||
@@ -845,13 +854,82 @@ fn path_for_buffer<'a>(
|
||||
if include_filename {
|
||||
Some(full_path.into())
|
||||
} else {
|
||||
Some(full_path.parent().unwrap().to_path_buf().into())
|
||||
Some(full_path.parent()?.to_path_buf().into())
|
||||
}
|
||||
} else {
|
||||
let mut path = file.path().strip_prefix(prefix).unwrap();
|
||||
let mut path = file.path().strip_prefix(prefix).ok()?;
|
||||
if !include_filename {
|
||||
path = path.parent().unwrap();
|
||||
path = path.parent()?;
|
||||
}
|
||||
Some(path.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::MutableAppContext;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut MutableAppContext) {
|
||||
let file = TestFile {
|
||||
path: Path::new("").into(),
|
||||
full_path: PathBuf::from(""),
|
||||
};
|
||||
assert_eq!(path_for_file(&file, 0, false, cx), None);
|
||||
}
|
||||
|
||||
struct TestFile {
|
||||
path: Arc<Path>,
|
||||
full_path: PathBuf,
|
||||
}
|
||||
|
||||
impl language::File for TestFile {
|
||||
fn path(&self) -> &Arc<Path> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
|
||||
self.full_path.clone()
|
||||
}
|
||||
|
||||
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn mtime(&self) -> std::time::SystemTime {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn is_deleted(&self) -> bool {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
_: u64,
|
||||
_: language::Rope,
|
||||
_: clock::Global,
|
||||
_: project::LineEnding,
|
||||
_: &mut MutableAppContext,
|
||||
) -> gpui::Task<anyhow::Result<(clock::Global, String, std::time::SystemTime)>> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn to_proto(&self) -> rpc::proto::File {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -811,7 +811,7 @@ mod tests {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let anchor_range = snapshot.anchor_before(selection_range.start)
|
||||
..snapshot.anchor_after(selection_range.end);
|
||||
editor.change_selections(Some(crate::Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
|
||||
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -677,6 +677,19 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn maybe_move_cursors_with(
|
||||
&mut self,
|
||||
mut update_cursor_position: impl FnMut(
|
||||
&DisplaySnapshot,
|
||||
DisplayPoint,
|
||||
SelectionGoal,
|
||||
) -> Option<(DisplayPoint, SelectionGoal)>,
|
||||
) {
|
||||
self.move_cursors_with(|map, point, goal| {
|
||||
update_cursor_position(map, point, goal).unwrap_or((point, goal))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replace_cursors_with(
|
||||
&mut self,
|
||||
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
|
||||
|
||||
@@ -76,7 +76,9 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path(file, None, true, cx)
|
||||
})
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
|
||||
self.editor.update(self.cx, |editor, cx| {
|
||||
editor.set_text(unmarked_text, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(selection_ranges)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@ impl FileFinder {
|
||||
match event {
|
||||
Event::Selected(project_path) => {
|
||||
workspace
|
||||
.open_path(project_path.clone(), true, cx)
|
||||
.open_path(project_path.clone(), None, true, cx)
|
||||
.detach_and_log_err(cx);
|
||||
workspace.dismiss_modal(cx);
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ impl GoToLine {
|
||||
if let Some(rows) = active_editor.highlighted_rows() {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
|
||||
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
}
|
||||
@@ -127,7 +127,7 @@ impl GoToLine {
|
||||
let display_point = point.to_display_point(&snapshot);
|
||||
let row = display_point.row();
|
||||
active_editor.highlight_rows(Some(row..row + 1));
|
||||
active_editor.request_autoscroll(Autoscroll::Center, cx);
|
||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -257,17 +257,19 @@ impl Element for Flex {
|
||||
let axis = self.axis;
|
||||
move |e, cx| {
|
||||
if remaining_space < 0. {
|
||||
let scroll_delta = e.delta.raw();
|
||||
|
||||
let mut delta = match axis {
|
||||
Axis::Horizontal => {
|
||||
if e.delta.x().abs() >= e.delta.y().abs() {
|
||||
e.delta.x()
|
||||
if scroll_delta.x().abs() >= scroll_delta.y().abs() {
|
||||
scroll_delta.x()
|
||||
} else {
|
||||
e.delta.y()
|
||||
scroll_delta.y()
|
||||
}
|
||||
}
|
||||
Axis::Vertical => e.delta.y(),
|
||||
Axis::Vertical => scroll_delta.y(),
|
||||
};
|
||||
if !e.precise {
|
||||
if !e.delta.precise() {
|
||||
delta *= 20.;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,8 +258,8 @@ impl Element for List {
|
||||
state.0.borrow_mut().scroll(
|
||||
&scroll_top,
|
||||
height,
|
||||
e.platform_event.delta,
|
||||
e.platform_event.precise,
|
||||
*e.platform_event.delta.raw(),
|
||||
e.platform_event.delta.precise(),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -295,15 +295,19 @@ impl Element for UniformList {
|
||||
move |MouseScrollWheel {
|
||||
platform_event:
|
||||
ScrollWheelEvent {
|
||||
position,
|
||||
delta,
|
||||
precise,
|
||||
..
|
||||
position, delta, ..
|
||||
},
|
||||
..
|
||||
},
|
||||
cx| {
|
||||
if !Self::scroll(state.clone(), position, delta, precise, scroll_max, cx) {
|
||||
if !Self::scroll(
|
||||
state.clone(),
|
||||
position,
|
||||
*delta.raw(),
|
||||
delta.precise(),
|
||||
scroll_max,
|
||||
cx,
|
||||
) {
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
|
||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -44,11 +46,45 @@ pub enum TouchPhase {
|
||||
Ended,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum ScrollDelta {
|
||||
Pixels(Vector2F),
|
||||
Lines(Vector2F),
|
||||
}
|
||||
|
||||
impl Default for ScrollDelta {
|
||||
fn default() -> Self {
|
||||
Self::Lines(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollDelta {
|
||||
pub fn raw(&self) -> &Vector2F {
|
||||
match self {
|
||||
ScrollDelta::Pixels(v) => v,
|
||||
ScrollDelta::Lines(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn precise(&self) -> bool {
|
||||
match self {
|
||||
ScrollDelta::Pixels(_) => true,
|
||||
ScrollDelta::Lines(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pixel_delta(&self, line_height: f32) -> Vector2F {
|
||||
match self {
|
||||
ScrollDelta::Pixels(delta) => *delta,
|
||||
ScrollDelta::Lines(delta) => vec2f(delta.x() * line_height, delta.y() * line_height),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ScrollWheelEvent {
|
||||
pub position: Vector2F,
|
||||
pub delta: Vector2F,
|
||||
pub precise: bool,
|
||||
pub delta: ScrollDelta,
|
||||
pub modifiers: Modifiers,
|
||||
/// If the platform supports returning the phase of a scroll wheel event, it will be stored here
|
||||
pub phase: Option<TouchPhase>,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
keymap::Keystroke,
|
||||
platform::{Event, NavigationDirection},
|
||||
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||
MouseMovedEvent, ScrollWheelEvent, TouchPhase,
|
||||
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||
};
|
||||
use cocoa::{
|
||||
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
|
||||
@@ -164,17 +164,24 @@ impl Event {
|
||||
_ => Some(TouchPhase::Moved),
|
||||
};
|
||||
|
||||
let raw_data = vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
);
|
||||
|
||||
let delta = if native_event.hasPreciseScrollingDeltas() == YES {
|
||||
ScrollDelta::Pixels(raw_data)
|
||||
} else {
|
||||
ScrollDelta::Lines(raw_data)
|
||||
};
|
||||
|
||||
Self::ScrollWheel(ScrollWheelEvent {
|
||||
position: vec2f(
|
||||
native_event.locationInWindow().x as f32,
|
||||
window_height - native_event.locationInWindow().y as f32,
|
||||
),
|
||||
delta: vec2f(
|
||||
native_event.scrollingDeltaX() as f32,
|
||||
native_event.scrollingDeltaY() as f32,
|
||||
),
|
||||
delta,
|
||||
phase,
|
||||
precise: native_event.hasPreciseScrollingDeltas() == YES,
|
||||
modifiers: read_modifiers(native_event),
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -16,4 +16,4 @@ chrono = "0.4"
|
||||
dirs = "4.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
settings = { path = "../settings" }
|
||||
shellexpand = "2.1.0"
|
||||
shellexpand = "2.1.0"
|
||||
|
||||
@@ -61,7 +61,7 @@ pub fn new_journal_entry(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
if let Some(editor) = item.downcast::<Editor>() {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
let len = editor.buffer().read(cx).len(cx);
|
||||
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([len..len])
|
||||
});
|
||||
if len > 0 {
|
||||
|
||||
@@ -72,4 +72,5 @@ tree-sitter-rust = "*"
|
||||
tree-sitter-python = "*"
|
||||
tree-sitter-typescript = "*"
|
||||
tree-sitter-ruby = "*"
|
||||
tree-sitter-embedded-template = "*"
|
||||
unindent = "0.1.7"
|
||||
|
||||
@@ -28,6 +28,7 @@ use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
mem,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
@@ -326,7 +327,13 @@ struct InjectionConfig {
|
||||
query: Query,
|
||||
content_capture_ix: u32,
|
||||
language_capture_ix: Option<u32>,
|
||||
languages_by_pattern_ix: Vec<Option<Box<str>>>,
|
||||
patterns: Vec<InjectionPatternConfig>,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct InjectionPatternConfig {
|
||||
language: Option<Box<str>>,
|
||||
combined: bool,
|
||||
}
|
||||
|
||||
struct BracketConfig {
|
||||
@@ -637,6 +644,10 @@ impl Language {
|
||||
self.adapter.clone()
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<usize> {
|
||||
self.grammar.as_ref().map(|g| g.id)
|
||||
}
|
||||
|
||||
pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self.grammar_mut();
|
||||
grammar.highlights_query = Some(Query::new(grammar.ts_language, source)?);
|
||||
@@ -730,15 +741,21 @@ impl Language {
|
||||
("content", &mut content_capture_ix),
|
||||
],
|
||||
);
|
||||
let languages_by_pattern_ix = (0..query.pattern_count())
|
||||
let patterns = (0..query.pattern_count())
|
||||
.map(|ix| {
|
||||
query.property_settings(ix).iter().find_map(|setting| {
|
||||
if setting.key.as_ref() == "language" {
|
||||
return setting.value.clone();
|
||||
} else {
|
||||
None
|
||||
let mut config = InjectionPatternConfig::default();
|
||||
for setting in query.property_settings(ix) {
|
||||
match setting.key.as_ref() {
|
||||
"language" => {
|
||||
config.language = setting.value.clone();
|
||||
}
|
||||
"combined" => {
|
||||
config.combined = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
})
|
||||
}
|
||||
config
|
||||
})
|
||||
.collect();
|
||||
if let Some(content_capture_ix) = content_capture_ix {
|
||||
@@ -746,7 +763,7 @@ impl Language {
|
||||
query,
|
||||
language_capture_ix,
|
||||
content_capture_ix,
|
||||
languages_by_pattern_ix,
|
||||
patterns,
|
||||
});
|
||||
}
|
||||
Ok(self)
|
||||
@@ -883,6 +900,20 @@ impl Language {
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for Language {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.id().hash(state)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Language {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id().eq(&other.id())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Language {}
|
||||
|
||||
impl Debug for Language {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Language")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -122,7 +122,7 @@ impl OutlineView {
|
||||
let display_rows = start.to_display_point(&snapshot).row()
|
||||
..end.to_display_point(&snapshot).row() + 1;
|
||||
active_editor.highlight_rows(Some(display_rows));
|
||||
active_editor.request_autoscroll(Autoscroll::Center, cx);
|
||||
active_editor.request_autoscroll(Autoscroll::center(), cx);
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
@@ -219,7 +219,7 @@ impl PickerDelegate for OutlineView {
|
||||
if let Some(rows) = active_editor.highlighted_rows() {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let position = DisplayPoint::new(rows.start, 0).to_point(&snapshot);
|
||||
active_editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
active_editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3453,29 +3453,41 @@ impl Project {
|
||||
let buffer_id = buffer.remote_id();
|
||||
|
||||
if self.is_local() {
|
||||
let lang_server = if let Some((_, server)) = self.language_server_for_buffer(buffer, cx)
|
||||
{
|
||||
server.clone()
|
||||
} else {
|
||||
return Task::ready(Ok(Default::default()));
|
||||
let lang_server = match self.language_server_for_buffer(buffer, cx) {
|
||||
Some((_, server)) => server.clone(),
|
||||
_ => return Task::ready(Ok(Default::default())),
|
||||
};
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let resolved_completion = lang_server
|
||||
.request::<lsp::request::ResolveCompletionItem>(completion.lsp_completion)
|
||||
.await?;
|
||||
|
||||
if let Some(edits) = resolved_completion.additional_text_edits {
|
||||
let edits = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.edits_from_lsp(&buffer_handle, edits, None, cx)
|
||||
})
|
||||
.await?;
|
||||
|
||||
buffer_handle.update(&mut cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.start_transaction();
|
||||
|
||||
for (range, text) in edits {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
let primary = &completion.old_range;
|
||||
let start_within = primary.start.cmp(&range.start, buffer).is_le()
|
||||
&& primary.end.cmp(&range.start, buffer).is_ge();
|
||||
let end_within = range.start.cmp(&primary.end, buffer).is_le()
|
||||
&& range.end.cmp(&primary.end, buffer).is_ge();
|
||||
|
||||
//Skip addtional edits which overlap with the primary completion edit
|
||||
//https://github.com/zed-industries/zed/pull/1871
|
||||
if !start_within && !end_within {
|
||||
buffer.edit([(range, text)], None, cx);
|
||||
}
|
||||
}
|
||||
|
||||
let transaction = if buffer.end_transaction(cx).is_some() {
|
||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||
if !push_to_history {
|
||||
@@ -3574,6 +3586,7 @@ impl Project {
|
||||
context: lsp::CodeActionContext {
|
||||
diagnostics: relevant_diagnostics,
|
||||
only: Some(vec![
|
||||
lsp::CodeActionKind::EMPTY,
|
||||
lsp::CodeActionKind::QUICKFIX,
|
||||
lsp::CodeActionKind::REFACTOR,
|
||||
lsp::CodeActionKind::REFACTOR_EXTRACT,
|
||||
|
||||
@@ -1179,6 +1179,10 @@ impl Snapshot {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn abs_path(&self) -> &Arc<Path> {
|
||||
&self.abs_path
|
||||
}
|
||||
|
||||
pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
|
||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||
}
|
||||
@@ -1370,10 +1374,6 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
impl LocalSnapshot {
|
||||
pub fn abs_path(&self) -> &Arc<Path> {
|
||||
&self.abs_path
|
||||
}
|
||||
|
||||
pub fn extension_counts(&self) -> &HashMap<OsString, usize> {
|
||||
&self.extension_counts
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
context_menu = { path = "../context_menu" }
|
||||
drag_and_drop = { path = "../drag_and_drop" }
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use drag_and_drop::{DragAndDrop, Draggable};
|
||||
use editor::{Cancel, Editor};
|
||||
use futures::stream::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
anyhow::{anyhow, Result},
|
||||
elements::{
|
||||
AnchorCorner, ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler,
|
||||
ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
AnchorCorner, ChildView, ConstrainedBox, ContainerStyle, Empty, Flex, Label,
|
||||
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||
},
|
||||
geometry::vector::Vector2F,
|
||||
impl_internal_actions, keymap,
|
||||
@@ -25,6 +26,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ProjectPanelEntry;
|
||||
use unicase::UniCase;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -41,6 +43,7 @@ pub struct ProjectPanel {
|
||||
filename_editor: ViewHandle<Editor>,
|
||||
clipboard_entry: Option<ClipboardEntry>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
dragged_entry_destination: Option<Arc<Path>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -71,8 +74,9 @@ pub enum ClipboardEntry {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct EntryDetails {
|
||||
pub struct EntryDetails {
|
||||
filename: String,
|
||||
path: Arc<Path>,
|
||||
depth: usize,
|
||||
kind: EntryKind,
|
||||
is_ignored: bool,
|
||||
@@ -92,6 +96,13 @@ pub struct Open {
|
||||
pub change_focus: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct MoveProjectEntry {
|
||||
pub entry_to_move: ProjectEntryId,
|
||||
pub destination: ProjectEntryId,
|
||||
pub destination_is_file: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployContextMenu {
|
||||
pub position: Vector2F,
|
||||
@@ -114,7 +125,10 @@ actions!(
|
||||
ToggleFocus
|
||||
]
|
||||
);
|
||||
impl_internal_actions!(project_panel, [Open, ToggleExpanded, DeployContextMenu]);
|
||||
impl_internal_actions!(
|
||||
project_panel,
|
||||
[Open, ToggleExpanded, DeployContextMenu, MoveProjectEntry]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ProjectPanel::deploy_context_menu);
|
||||
@@ -138,6 +152,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
this.paste(action, cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(ProjectPanel::move_entry);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
@@ -216,10 +231,12 @@ impl ProjectPanel {
|
||||
filename_editor,
|
||||
clipboard_entry: None,
|
||||
context_menu: cx.add_view(ContextMenu::new),
|
||||
dragged_entry_destination: None,
|
||||
};
|
||||
this.update_visible_entries(None, cx);
|
||||
this
|
||||
});
|
||||
|
||||
cx.subscribe(&project_panel, {
|
||||
let project_panel = project_panel.downgrade();
|
||||
move |workspace, _, event, cx| match event {
|
||||
@@ -235,6 +252,7 @@ impl ProjectPanel {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: entry.path.clone(),
|
||||
},
|
||||
None,
|
||||
focus_opened_item,
|
||||
cx,
|
||||
)
|
||||
@@ -601,6 +619,10 @@ impl ProjectPanel {
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
|
||||
drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,6 +787,39 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_entry(
|
||||
&mut self,
|
||||
&MoveProjectEntry {
|
||||
entry_to_move,
|
||||
destination,
|
||||
destination_is_file,
|
||||
}: &MoveProjectEntry,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let destination_worktree = self.project.update(cx, |project, cx| {
|
||||
let entry_path = project.path_for_entry(entry_to_move, cx)?;
|
||||
let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
|
||||
|
||||
let mut destination_path = destination_entry_path.as_ref();
|
||||
if destination_is_file {
|
||||
destination_path = destination_path.parent()?;
|
||||
}
|
||||
|
||||
let mut new_path = destination_path.to_path_buf();
|
||||
new_path.push(entry_path.path.file_name()?);
|
||||
if new_path != entry_path.path.as_ref() {
|
||||
let task = project.rename_entry(entry_to_move, new_path, cx)?;
|
||||
cx.foreground().spawn(task).detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
Some(project.worktree_id_for_entry(destination, cx)?)
|
||||
});
|
||||
|
||||
if let Some(destination_worktree) = destination_worktree {
|
||||
self.expand_entry(destination_worktree, destination, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
|
||||
let mut entry_index = 0;
|
||||
let mut visible_entries_index = 0;
|
||||
@@ -950,14 +1005,15 @@ impl ProjectPanel {
|
||||
let end_ix = range.end.min(ix + visible_worktree_entries.len());
|
||||
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
let expanded_entry_ids = self
|
||||
.expanded_dir_ids
|
||||
.get(&snapshot.id())
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or(&[]);
|
||||
let root_name = OsStr::new(snapshot.root_name());
|
||||
for entry in &visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
|
||||
{
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
for entry in &visible_worktree_entries[entry_range] {
|
||||
let mut details = EntryDetails {
|
||||
filename: entry
|
||||
.path
|
||||
@@ -965,6 +1021,7 @@ impl ProjectPanel {
|
||||
.unwrap_or(root_name)
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
path: entry.path.clone(),
|
||||
depth: entry.path.components().count(),
|
||||
kind: entry.kind,
|
||||
is_ignored: entry.is_ignored,
|
||||
@@ -978,12 +1035,14 @@ impl ProjectPanel {
|
||||
.clipboard_entry
|
||||
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
|
||||
};
|
||||
|
||||
if let Some(edit_state) = &self.edit_state {
|
||||
let is_edited_entry = if edit_state.is_new_entry {
|
||||
entry.id == NEW_ENTRY_ID
|
||||
} else {
|
||||
entry.id == edit_state.entry_id
|
||||
};
|
||||
|
||||
if is_edited_entry {
|
||||
if let Some(processing_filename) = &edit_state.processing_filename {
|
||||
details.is_processing = true;
|
||||
@@ -1005,77 +1064,115 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_entry_visual_element<V: View>(
|
||||
details: &EntryDetails,
|
||||
editor: Option<&ViewHandle<Editor>>,
|
||||
padding: f32,
|
||||
row_container_style: ContainerStyle,
|
||||
style: &ProjectPanelEntry,
|
||||
cx: &mut RenderContext<V>,
|
||||
) -> ElementBox {
|
||||
let kind = details.kind;
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ConstrainedBox::new(if kind == EntryKind::Dir {
|
||||
if details.is_expanded {
|
||||
Svg::new("icons/chevron_down_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
} else {
|
||||
Svg::new("icons/chevron_right_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
}
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_max_width(style.icon_size)
|
||||
.with_max_height(style.icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(if show_editor && editor.is_some() {
|
||||
ChildView::new(editor.unwrap().clone(), cx)
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1.0, true)
|
||||
.boxed()
|
||||
} else {
|
||||
Label::new(details.filename.clone(), style.text.clone())
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
})
|
||||
.constrained()
|
||||
.with_height(style.height)
|
||||
.contained()
|
||||
.with_style(row_container_style)
|
||||
.with_padding_left(padding)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_entry(
|
||||
entry_id: ProjectEntryId,
|
||||
details: EntryDetails,
|
||||
editor: &ViewHandle<Editor>,
|
||||
dragged_entry_destination: &mut Option<Arc<Path>>,
|
||||
theme: &theme::ProjectPanel,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let this = cx.handle();
|
||||
let kind = details.kind;
|
||||
let path = details.path.clone();
|
||||
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
|
||||
|
||||
let entry_style = if details.is_cut {
|
||||
&theme.cut_entry
|
||||
} else if details.is_ignored {
|
||||
&theme.ignored_entry
|
||||
} else {
|
||||
&theme.entry
|
||||
};
|
||||
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
MouseEventHandler::<Self>::new(entry_id.to_usize(), cx, |state, cx| {
|
||||
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
|
||||
let mut style = entry_style.style_for(state, details.is_selected).clone();
|
||||
|
||||
let entry_style = if details.is_cut {
|
||||
&theme.cut_entry
|
||||
} else if details.is_ignored {
|
||||
&theme.ignored_entry
|
||||
} else {
|
||||
&theme.entry
|
||||
};
|
||||
|
||||
let style = entry_style.style_for(state, details.is_selected).clone();
|
||||
if cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
&& dragged_entry_destination
|
||||
.as_ref()
|
||||
.filter(|destination| details.path.starts_with(destination))
|
||||
.is_some()
|
||||
{
|
||||
style = entry_style.active.clone().unwrap();
|
||||
}
|
||||
|
||||
let row_container_style = if show_editor {
|
||||
theme.filename_editor.container
|
||||
} else {
|
||||
style.container
|
||||
};
|
||||
Flex::row()
|
||||
.with_child(
|
||||
ConstrainedBox::new(if kind == EntryKind::Dir {
|
||||
if details.is_expanded {
|
||||
Svg::new("icons/chevron_down_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
} else {
|
||||
Svg::new("icons/chevron_right_8.svg")
|
||||
.with_color(style.icon_color)
|
||||
.boxed()
|
||||
}
|
||||
} else {
|
||||
Empty::new().boxed()
|
||||
})
|
||||
.with_max_width(style.icon_size)
|
||||
.with_max_height(style.icon_size)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(if show_editor {
|
||||
ChildView::new(editor.clone(), cx)
|
||||
.contained()
|
||||
.with_margin_left(theme.entry.default.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1.0, true)
|
||||
.boxed()
|
||||
} else {
|
||||
Label::new(details.filename, style.text.clone())
|
||||
.contained()
|
||||
.with_margin_left(style.icon_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
})
|
||||
.constrained()
|
||||
.with_height(theme.entry.default.height)
|
||||
.contained()
|
||||
.with_style(row_container_style)
|
||||
.with_padding_left(padding)
|
||||
.boxed()
|
||||
|
||||
Self::render_entry_visual_element(
|
||||
&details,
|
||||
Some(editor),
|
||||
padding,
|
||||
row_container_style,
|
||||
&style,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(MouseButton::Left, move |e, cx| {
|
||||
if kind == EntryKind::Dir {
|
||||
@@ -1093,6 +1190,50 @@ impl ProjectPanel {
|
||||
position: e.position,
|
||||
})
|
||||
})
|
||||
.on_up(MouseButton::Left, move |_, cx| {
|
||||
if let Some((_, dragged_entry)) = cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
{
|
||||
cx.dispatch_action(MoveProjectEntry {
|
||||
entry_to_move: *dragged_entry,
|
||||
destination: entry_id,
|
||||
destination_is_file: matches!(details.kind, EntryKind::File(_)),
|
||||
});
|
||||
}
|
||||
})
|
||||
.on_move(move |_, cx| {
|
||||
if cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
{
|
||||
if let Some(this) = this.upgrade(cx.app) {
|
||||
this.update(cx.app, |this, _| {
|
||||
this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
|
||||
path.parent().map(|parent| Arc::from(parent))
|
||||
} else {
|
||||
Some(path.clone())
|
||||
};
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.as_draggable(entry_id, {
|
||||
let row_container_style = theme.dragged_entry.container;
|
||||
|
||||
move |_, cx: &mut RenderContext<Workspace>| {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
Self::render_entry_visual_element(
|
||||
&details,
|
||||
None,
|
||||
padding,
|
||||
row_container_style,
|
||||
&theme.project_panel.dragged_entry,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.boxed()
|
||||
}
|
||||
@@ -1104,14 +1245,15 @@ impl View for ProjectPanel {
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
|
||||
enum Tag {}
|
||||
enum ProjectPanel {}
|
||||
let theme = &cx.global::<Settings>().theme.project_panel;
|
||||
let mut container_style = theme.container;
|
||||
let padding = std::mem::take(&mut container_style.padding);
|
||||
let last_worktree_root_id = self.last_worktree_root_id;
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Tag>::new(0, cx, |_, cx| {
|
||||
MouseEventHandler::<ProjectPanel>::new(0, cx, |_, cx| {
|
||||
UniformList::new(
|
||||
self.list.clone(),
|
||||
self.visible_entries
|
||||
@@ -1121,15 +1263,19 @@ impl View for ProjectPanel {
|
||||
cx,
|
||||
move |this, range, items, cx| {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let mut dragged_entry_destination =
|
||||
this.dragged_entry_destination.clone();
|
||||
this.for_each_visible_entry(range, cx, |id, details, cx| {
|
||||
items.push(Self::render_entry(
|
||||
id,
|
||||
details,
|
||||
&this.filename_editor,
|
||||
&mut dragged_entry_destination,
|
||||
&theme.project_panel,
|
||||
cx,
|
||||
));
|
||||
});
|
||||
this.dragged_entry_destination = dragged_entry_destination;
|
||||
},
|
||||
)
|
||||
.with_padding_top(padding.top)
|
||||
|
||||
@@ -28,4 +28,4 @@ settings = { path = "../settings", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
language = { path = "../language", features = ["test-support"] }
|
||||
lsp = { path = "../lsp", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
|
||||
@@ -150,7 +150,7 @@ impl ProjectSymbolsView {
|
||||
|
||||
let editor = workspace.open_project_item::<Editor>(buffer, cx);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Center), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
});
|
||||
|
||||
@@ -512,7 +512,7 @@ impl ProjectSearchView {
|
||||
let range_to_select = match_ranges[new_index].clone();
|
||||
self.results_editor.update(cx, |editor, cx| {
|
||||
editor.unfold_ranges([range_to_select.clone()], false, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges([range_to_select])
|
||||
});
|
||||
});
|
||||
@@ -546,7 +546,7 @@ impl ProjectSearchView {
|
||||
} else {
|
||||
self.results_editor.update(cx, |editor, cx| {
|
||||
if reset_selections {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(match_ranges.first().cloned())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -36,18 +36,6 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
///This function checks if to_esc_str would work, assuming all terminal settings are off.
|
||||
///Note that this function is conservative. It can fail in cases where the actual to_esc_str succeeds.
|
||||
///This is unavoidable for our use case. GPUI cannot wait until we acquire the terminal
|
||||
///lock to determine whether we could actually send the keystroke with the current settings. Therefore,
|
||||
///This conservative guess is used instead. Note that in practice the case where this method
|
||||
///Returns false when the actual terminal would consume the keystroke never happens. All keystrokes
|
||||
///that depend on terminal modes also have a mapping that doesn't depend on the terminal mode.
|
||||
///This is fragile, but as these mappings are locked up in legacy compatibility, it's probably good enough
|
||||
pub fn might_convert(keystroke: &Keystroke) -> bool {
|
||||
to_esc_str(keystroke, &TermMode::NONE, false).is_some()
|
||||
}
|
||||
|
||||
pub fn to_esc_str(keystroke: &Keystroke, mode: &TermMode, alt_is_meta: bool) -> Option<String> {
|
||||
let modifiers = Modifiers::new(keystroke);
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ impl MouseButton {
|
||||
}
|
||||
|
||||
fn from_scroll(e: &ScrollWheelEvent) -> Self {
|
||||
if e.delta.y() > 0. {
|
||||
if e.delta.raw().y() > 0. {
|
||||
MouseButton::ScrollUp
|
||||
} else {
|
||||
MouseButton::ScrollDown
|
||||
|
||||
@@ -407,13 +407,18 @@ impl TerminalBuilder {
|
||||
'outer: loop {
|
||||
let mut events = vec![];
|
||||
let mut timer = cx.background().timer(Duration::from_millis(4)).fuse();
|
||||
|
||||
let mut wakeup = false;
|
||||
loop {
|
||||
futures::select_biased! {
|
||||
_ = timer => break,
|
||||
event = self.events_rx.next() => {
|
||||
if let Some(event) = event {
|
||||
events.push(event);
|
||||
if matches!(event, AlacTermEvent::Wakeup) {
|
||||
wakeup = true;
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
if events.len() > 100 {
|
||||
break;
|
||||
}
|
||||
@@ -432,6 +437,9 @@ impl TerminalBuilder {
|
||||
for event in events {
|
||||
this.process_event(&event, cx);
|
||||
}
|
||||
if wakeup {
|
||||
this.process_event(&AlacTermEvent::Wakeup, cx);
|
||||
}
|
||||
});
|
||||
smol::future::yield_now().await;
|
||||
}
|
||||
@@ -627,7 +635,7 @@ impl Terminal {
|
||||
term.grid_mut().reset_region(..cursor.line);
|
||||
|
||||
// Copy the current line up
|
||||
let line = term.grid()[cursor.line][..cursor.column]
|
||||
let line = term.grid()[cursor.line][..Column(term.grid().columns())]
|
||||
.iter()
|
||||
.cloned()
|
||||
.enumerate()
|
||||
@@ -1136,7 +1144,7 @@ impl Terminal {
|
||||
|
||||
fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
|
||||
let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
|
||||
|
||||
let line_height = self.last_content.size.line_height;
|
||||
match e.phase {
|
||||
/* Reset scroll state on started */
|
||||
Some(gpui::TouchPhase::Started) => {
|
||||
@@ -1145,11 +1153,11 @@ impl Terminal {
|
||||
}
|
||||
/* Calculate the appropriate scroll lines */
|
||||
Some(gpui::TouchPhase::Moved) => {
|
||||
let old_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
|
||||
let old_offset = (self.scroll_px / line_height) as i32;
|
||||
|
||||
self.scroll_px += e.delta.y() * scroll_multiplier;
|
||||
self.scroll_px += e.delta.pixel_delta(line_height).y() * scroll_multiplier;
|
||||
|
||||
let new_offset = (self.scroll_px / self.last_content.size.line_height) as i32;
|
||||
let new_offset = (self.scroll_px / line_height) as i32;
|
||||
|
||||
// Whenever we hit the edges, reset our stored scroll to 0
|
||||
// so we can respond to changes in direction quickly
|
||||
@@ -1159,7 +1167,7 @@ impl Terminal {
|
||||
}
|
||||
/* Fall back to delta / line_height */
|
||||
None => Some(
|
||||
((e.delta.y() * scroll_multiplier) / self.last_content.size.line_height) as i32,
|
||||
((e.delta.pixel_delta(line_height).y() * scroll_multiplier) / line_height) as i32,
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
|
||||
@@ -326,6 +326,7 @@ pub struct ProjectPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub entry: Interactive<ProjectPanelEntry>,
|
||||
pub dragged_entry: ProjectPanelEntry,
|
||||
pub ignored_entry: Interactive<ProjectPanelEntry>,
|
||||
pub cut_entry: Interactive<ProjectPanelEntry>,
|
||||
pub filename_editor: FieldEditor,
|
||||
|
||||
@@ -15,4 +15,4 @@ settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace" }
|
||||
project = { path = "../project" }
|
||||
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
|
||||
@@ -42,4 +42,4 @@ language = { path = "../language", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
settings = { path = "../settings" }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
||||
@@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
|
||||
|
||||
@@ -137,6 +137,11 @@ impl Motion {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn infallible(self) -> bool {
|
||||
use Motion::*;
|
||||
matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
|
||||
}
|
||||
|
||||
pub fn inclusive(self) -> bool {
|
||||
use Motion::*;
|
||||
match self {
|
||||
@@ -164,9 +169,9 @@ impl Motion {
|
||||
point: DisplayPoint,
|
||||
goal: SelectionGoal,
|
||||
times: usize,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
) -> Option<(DisplayPoint, SelectionGoal)> {
|
||||
use Motion::*;
|
||||
match self {
|
||||
let (new_point, goal) = match self {
|
||||
Left => (left(map, point, times), SelectionGoal::None),
|
||||
Backspace => (backspace(map, point, times), SelectionGoal::None),
|
||||
Down => down(map, point, goal, times),
|
||||
@@ -191,7 +196,9 @@ impl Motion {
|
||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
|
||||
Matching => (matching(map, point), SelectionGoal::None),
|
||||
}
|
||||
};
|
||||
|
||||
(new_point != point || self.infallible()).then_some((new_point, goal))
|
||||
}
|
||||
|
||||
// Expands a selection using self motion for an operator
|
||||
@@ -201,12 +208,13 @@ impl Motion {
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: usize,
|
||||
expand_to_surrounding_newline: bool,
|
||||
) {
|
||||
let (new_head, goal) = self.move_point(map, selection.head(), selection.goal, times);
|
||||
selection.set_head(new_head, goal);
|
||||
) -> bool {
|
||||
if let Some((new_head, goal)) =
|
||||
self.move_point(map, selection.head(), selection.goal, times)
|
||||
{
|
||||
selection.set_head(new_head, goal);
|
||||
|
||||
if self.linewise() {
|
||||
if selection.start != selection.end {
|
||||
if self.linewise() {
|
||||
selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
|
||||
|
||||
if expand_to_surrounding_newline {
|
||||
@@ -215,7 +223,7 @@ impl Motion {
|
||||
*selection.end.column_mut() = 0;
|
||||
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||
// Don't reset the end here
|
||||
return;
|
||||
return true;
|
||||
} else if selection.start.row() > 0 {
|
||||
*selection.start.row_mut() -= 1;
|
||||
*selection.start.column_mut() = map.line_len(selection.start.row());
|
||||
@@ -224,31 +232,33 @@ impl Motion {
|
||||
}
|
||||
|
||||
(_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
|
||||
}
|
||||
} else {
|
||||
// If the motion is exclusive and the end of the motion is in column 1, the
|
||||
// end of the motion is moved to the end of the previous line and the motion
|
||||
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
|
||||
// but "d}" will not include that line.
|
||||
let mut inclusive = self.inclusive();
|
||||
if !inclusive
|
||||
&& self != Motion::Backspace
|
||||
&& selection.end.row() > selection.start.row()
|
||||
&& selection.end.column() == 0
|
||||
&& selection.end.row() > 0
|
||||
{
|
||||
inclusive = true;
|
||||
*selection.end.row_mut() -= 1;
|
||||
*selection.end.column_mut() = 0;
|
||||
selection.end = map.clip_point(
|
||||
map.next_line_boundary(selection.end.to_point(map)).1,
|
||||
Bias::Left,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If the motion is exclusive and the end of the motion is in column 1, the
|
||||
// end of the motion is moved to the end of the previous line and the motion
|
||||
// becomes inclusive. Example: "}" moves to the first line after a paragraph,
|
||||
// but "d}" will not include that line.
|
||||
let mut inclusive = self.inclusive();
|
||||
if !inclusive
|
||||
&& self != Motion::Backspace
|
||||
&& selection.end.row() > selection.start.row()
|
||||
&& selection.end.column() == 0
|
||||
{
|
||||
inclusive = true;
|
||||
*selection.end.row_mut() -= 1;
|
||||
*selection.end.column_mut() = 0;
|
||||
selection.end = map.clip_point(
|
||||
map.next_line_boundary(selection.end.to_point(map)).1,
|
||||
Bias::Left,
|
||||
);
|
||||
}
|
||||
|
||||
if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
|
||||
*selection.end.column_mut() += 1;
|
||||
if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
|
||||
*selection.end.column_mut() += 1;
|
||||
}
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -256,7 +266,7 @@ impl Motion {
|
||||
fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
|
||||
for _ in 0..times {
|
||||
*point.column_mut() = point.column().saturating_sub(1);
|
||||
point = map.clip_point(point, Bias::Right);
|
||||
point = map.clip_point(point, Bias::Left);
|
||||
if point.column() == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -325,9 +335,7 @@ pub(crate) fn next_word_start(
|
||||
|| at_newline && crossed_newline
|
||||
|| at_newline && left == '\n'; // Prevents skipping repeated empty lines
|
||||
|
||||
if at_newline {
|
||||
crossed_newline = true;
|
||||
}
|
||||
crossed_newline |= at_newline;
|
||||
found
|
||||
})
|
||||
}
|
||||
@@ -350,7 +358,7 @@ fn next_word_end(
|
||||
});
|
||||
|
||||
// find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
|
||||
// we have backtraced already
|
||||
// we have backtracked already
|
||||
if !map
|
||||
.chars_at(point)
|
||||
.nth(1)
|
||||
|
||||
@@ -114,8 +114,12 @@ pub fn normal_object(object: Object, cx: &mut MutableAppContext) {
|
||||
|
||||
fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| motion.move_point(map, cursor, goal, times))
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
motion
|
||||
.move_point(map, cursor, goal, times)
|
||||
.unwrap_or((cursor, goal))
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -124,8 +128,8 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::Right.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
});
|
||||
@@ -141,8 +145,8 @@ fn insert_first_non_whitespace(
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::FirstNonWhitespace.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
});
|
||||
@@ -154,8 +158,8 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
});
|
||||
@@ -183,7 +187,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||
(start_of_line..start_of_line, new_text)
|
||||
});
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.row_mut() -= 1;
|
||||
*cursor.column_mut() = map.line_len(cursor.row());
|
||||
@@ -214,8 +218,8 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
||||
new_text.push_str(&" ".repeat(indent as usize));
|
||||
(end_of_line..end_of_line, new_text)
|
||||
});
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
s.move_cursors_with(|map, cursor, goal| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.maybe_move_cursors_with(|map, cursor, goal| {
|
||||
Motion::EndOfLine.move_point(map, cursor, goal, 1)
|
||||
});
|
||||
});
|
||||
@@ -332,7 +336,7 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
|
||||
});
|
||||
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if let Some(new_position) = new_selections.get(&selection.id) {
|
||||
match new_position {
|
||||
@@ -847,4 +851,10 @@ mod test {
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
|
||||
cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
|
||||
use editor::{char_kind, display_map::DisplaySnapshot, movement, Autoscroll, DisplayPoint};
|
||||
use editor::{
|
||||
char_kind, display_map::DisplaySnapshot, movement, Autoscroll, CharKind, DisplayPoint,
|
||||
};
|
||||
use gpui::MutableAppContext;
|
||||
use language::Selection;
|
||||
|
||||
pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
// Some motions ignore failure when switching to normal mode
|
||||
let mut motion_succeeded = matches!(
|
||||
motion,
|
||||
Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine
|
||||
);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if let Motion::NextWordStart { ignore_punctuation } = motion {
|
||||
expand_changed_word_selection(map, selection, times, ignore_punctuation);
|
||||
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
|
||||
{
|
||||
expand_changed_word_selection(map, selection, times, ignore_punctuation)
|
||||
} else {
|
||||
motion.expand_selection(map, selection, times, false);
|
||||
}
|
||||
motion.expand_selection(map, selection, times, false)
|
||||
};
|
||||
});
|
||||
});
|
||||
copy_selections_content(editor, motion.linewise(), cx);
|
||||
editor.insert("", cx);
|
||||
});
|
||||
});
|
||||
vim.switch_mode(Mode::Insert, false, cx)
|
||||
|
||||
if motion_succeeded {
|
||||
vim.switch_mode(Mode::Insert, false, cx)
|
||||
} else {
|
||||
vim.switch_mode(Mode::Normal, false, cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut MutableAppContext) {
|
||||
@@ -30,7 +43,7 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
objects_found |= object.expand_selection(map, selection, around);
|
||||
});
|
||||
@@ -49,36 +62,45 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||
}
|
||||
}
|
||||
|
||||
// From the docs https://vimhelp.org/change.txt.html#cw
|
||||
// Special case: When the cursor is in a word, "cw" and "cW" do not include the
|
||||
// white space after a word, they only change up to the end of the word. This is
|
||||
// because Vim interprets "cw" as change-word, and a word does not include the
|
||||
// following white space.
|
||||
// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
|
||||
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
|
||||
// on a non-blank. This is because "cw" is interpreted as change-word, and a
|
||||
// word does not include the following white space. {Vi: "cw" when on a blank
|
||||
// followed by other blanks changes only the first blank; this is probably a
|
||||
// bug, because "dw" deletes all the blanks}
|
||||
//
|
||||
// NOT HANDLED YET
|
||||
// Another special case: When using the "w" motion in combination with an
|
||||
// operator and the last word moved over is at the end of a line, the end of
|
||||
// that word becomes the end of the operated text, not the first word in the
|
||||
// next line.
|
||||
fn expand_changed_word_selection(
|
||||
map: &DisplaySnapshot,
|
||||
selection: &mut Selection<DisplayPoint>,
|
||||
times: usize,
|
||||
ignore_punctuation: bool,
|
||||
) {
|
||||
if times > 1 {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(
|
||||
map,
|
||||
selection,
|
||||
times - 1,
|
||||
false,
|
||||
);
|
||||
) -> bool {
|
||||
if times == 1 {
|
||||
let in_word = map
|
||||
.chars_at(selection.head())
|
||||
.next()
|
||||
.map(|(c, _)| char_kind(c) != CharKind::Whitespace)
|
||||
.unwrap_or_default();
|
||||
|
||||
if in_word {
|
||||
selection.end = movement::find_boundary(map, selection.end, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
left_kind != right_kind && left_kind != CharKind::Whitespace
|
||||
});
|
||||
true
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false)
|
||||
}
|
||||
} else {
|
||||
Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false)
|
||||
}
|
||||
|
||||
if times == 1 && selection.end.column() == map.line_len(selection.end.row()) {
|
||||
return;
|
||||
}
|
||||
|
||||
selection.end = movement::find_boundary(map, selection.end, |left, right| {
|
||||
let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
|
||||
let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
|
||||
|
||||
left_kind != right_kind || left == '\n' || right == '\n'
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -8,7 +8,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut Mutab
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let original_head = selection.head();
|
||||
original_columns.insert(selection.id, original_head.column());
|
||||
@@ -20,7 +20,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut Mutab
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head();
|
||||
if motion.linewise() {
|
||||
@@ -43,7 +43,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||
// Emulates behavior in vim where if we expanded backwards to include a newline
|
||||
// the cursor gets set back to the start of the line
|
||||
let mut should_move_to_start: HashSet<_> = Default::default();
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
object.expand_selection(map, selection, around);
|
||||
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
|
||||
@@ -78,7 +78,7 @@ pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Mutab
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head();
|
||||
if should_move_to_start.contains(&selection.id) {
|
||||
@@ -143,7 +143,7 @@ mod test {
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
ExemptionFeatures::DeletionOnEmptyLine,
|
||||
ExemptionFeatures::DeleteWordOnEmptyLine,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -169,7 +169,7 @@ mod test {
|
||||
Test test
|
||||
ˇ
|
||||
test"},
|
||||
ExemptionFeatures::DeletionOnEmptyLine,
|
||||
ExemptionFeatures::OperatorLastNewlineRemains,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ use util::test::marked_text_offsets;
|
||||
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||
use crate::state::Mode;
|
||||
|
||||
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[];
|
||||
pub const SUPPORTED_FEATURES: &[ExemptionFeatures] = &[
|
||||
ExemptionFeatures::DeletionOnEmptyLine,
|
||||
ExemptionFeatures::OperatorAbortsOnFailedMotion,
|
||||
];
|
||||
|
||||
/// Enum representing features we have tests for but which don't work, yet. Used
|
||||
/// to add exemptions and automatically
|
||||
@@ -19,6 +22,10 @@ pub enum ExemptionFeatures {
|
||||
DeletionOnEmptyLine,
|
||||
// When a motion fails, it should should not apply linewise operations
|
||||
OperatorAbortsOnFailedMotion,
|
||||
// When an operator completes at the end of the file, an extra newline is left
|
||||
OperatorLastNewlineRemains,
|
||||
// Deleting a word on an empty line doesn't remove the newline
|
||||
DeleteWordOnEmptyLine,
|
||||
|
||||
// OBJECTS
|
||||
// Resulting position after the operation is slightly incorrect for unintuitive reasons.
|
||||
|
||||
@@ -67,7 +67,9 @@ impl<'a> VimTestContext<'a> {
|
||||
|
||||
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
|
||||
let item = workspace
|
||||
.update(cx, |workspace, cx| workspace.open_path(file, true, cx))
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.open_path(file, None, true, cx)
|
||||
})
|
||||
.await
|
||||
.expect("Could not open test file");
|
||||
|
||||
|
||||
@@ -26,24 +26,27 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
pub fn visual_motion(motion: Motion, times: usize, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let was_reversed = selection.reversed;
|
||||
|
||||
let (new_head, goal) =
|
||||
motion.move_point(map, selection.head(), selection.goal, times);
|
||||
selection.set_head(new_head, goal);
|
||||
if let Some((new_head, goal)) =
|
||||
motion.move_point(map, selection.head(), selection.goal, times)
|
||||
{
|
||||
selection.set_head(new_head, goal);
|
||||
|
||||
if was_reversed && !selection.reversed {
|
||||
// Head was at the start of the selection, and now is at the end. We need to move the start
|
||||
// back by one if possible in order to compensate for this change.
|
||||
*selection.start.column_mut() = selection.start.column().saturating_sub(1);
|
||||
selection.start = map.clip_point(selection.start, Bias::Left);
|
||||
} else if !was_reversed && selection.reversed {
|
||||
// Head was at the end of the selection, and now is at the start. We need to move the end
|
||||
// forward by one if possible in order to compensate for this change.
|
||||
*selection.end.column_mut() = selection.end.column() + 1;
|
||||
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||
if was_reversed && !selection.reversed {
|
||||
// Head was at the start of the selection, and now is at the end. We need to move the start
|
||||
// back by one if possible in order to compensate for this change.
|
||||
*selection.start.column_mut() =
|
||||
selection.start.column().saturating_sub(1);
|
||||
selection.start = map.clip_point(selection.start, Bias::Left);
|
||||
} else if !was_reversed && selection.reversed {
|
||||
// Head was at the end of the selection, and now is at the start. We need to move the end
|
||||
// forward by one if possible in order to compensate for this change.
|
||||
*selection.end.column_mut() = selection.end.column() + 1;
|
||||
selection.end = map.clip_point(selection.end, Bias::Right);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -55,7 +58,7 @@ pub fn visual_object(object: Object, cx: &mut MutableAppContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
if let Operator::Object { around } = vim.pop_operator(cx) {
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let head = selection.head();
|
||||
if let Some(mut range) = object.range(map, head, around) {
|
||||
@@ -123,7 +126,7 @@ pub fn change(_: &mut Workspace, _: &VisualChange, cx: &mut ViewContext<Workspac
|
||||
});
|
||||
copy_selections_content(editor, editor.selections.line_mode, cx);
|
||||
editor.edit_with_autoindent(edits, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_anchors(new_selections);
|
||||
});
|
||||
});
|
||||
@@ -137,7 +140,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
let line_mode = editor.selections.line_mode;
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if line_mode {
|
||||
original_columns
|
||||
@@ -156,7 +159,7 @@ pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspac
|
||||
|
||||
// Fixup cursor position after the deletion
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
let mut cursor = selection.head().to_point(map);
|
||||
|
||||
@@ -295,7 +298,7 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
|
||||
buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
|
||||
});
|
||||
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select(new_selections)
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -1 +1 @@
|
||||
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
||||
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over\n"},{"Mode":"Insert"},{"Selection":{"start":[3,0],"end":[3,0]}},{"Mode":"Insert"}]
|
||||
@@ -1 +1 @@
|
||||
[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
|
||||
[{"Text":"\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":""},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"\nbrown fox\njumps over\nthe lazy"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
|
||||
@@ -1 +1 @@
|
||||
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"}]
|
||||
[{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,6],"end":[2,6]}},{"Mode":"Normal"},{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\n"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
|
||||
@@ -1 +1 @@
|
||||
[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"}]
|
||||
[{"Text":"\njumps over"},{"Mode":"Insert"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Insert"},{"Text":"The quick\n"},{"Mode":"Insert"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Insert"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
|
||||
@@ -1 +1 @@
|
||||
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"}]
|
||||
[{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,6],"end":[1,6]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox"},{"Mode":"Normal"},{"Selection":{"start":[1,0],"end":[1,0]}},{"Mode":"Normal"}]
|
||||
@@ -1 +1 @@
|
||||
[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"}]
|
||||
[{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,5],"end":[2,5]}},{"Mode":"Normal"},{"Text":"The quick\nbrown fox\njumps over"},{"Mode":"Normal"},{"Selection":{"start":[2,0],"end":[2,0]}},{"Mode":"Normal"}]
|
||||
@@ -1 +1 @@
|
||||
[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
|
||||
[{"Text":"jumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":""},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,5],"end":[0,5]}},{"Mode":"Normal"},{"Text":"brown fox\njumps over\nthe lazy"},{"Mode":"Normal"},{"Selection":{"start":[0,0],"end":[0,0]}},{"Mode":"Normal"}]
|
||||
1
crates/vim/test_data/test_h_through_unicode.json
Normal file
1
crates/vim/test_data/test_h_through_unicode.json
Normal file
@@ -0,0 +1 @@
|
||||
[{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,3],"end":[0,3]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,4],"end":[0,4]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,10],"end":[0,10]}},{"Mode":"Normal"},{"Text":"Test├──┐Test"},{"Mode":"Normal"},{"Selection":{"start":[0,13],"end":[0,13]}},{"Mode":"Normal"}]
|
||||
File diff suppressed because one or more lines are too long
@@ -46,4 +46,4 @@ client = { path = "../client", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
|
||||
@@ -7,9 +7,13 @@ use gpui::{
|
||||
AppContext, Element, ElementBox, EventContext, MouseButton, MouseState, Quad, RenderContext,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use project::ProjectEntryId;
|
||||
use settings::Settings;
|
||||
|
||||
use crate::{MoveItem, Pane, SplitDirection, SplitWithItem, Workspace};
|
||||
use crate::{
|
||||
MoveItem, OpenProjectEntryInPane, Pane, SplitDirection, SplitWithItem, SplitWithProjectEntry,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
use super::DraggedItem;
|
||||
|
||||
@@ -28,12 +32,18 @@ where
|
||||
MouseEventHandler::<Tag>::above(region_id, cx, |state, cx| {
|
||||
// Observing hovered will cause a render when the mouse enters regardless
|
||||
// of if mouse position was accessed before
|
||||
let hovered = state.hovered();
|
||||
let drag_position = cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.filter(|_| hovered)
|
||||
.map(|(drag_position, _)| drag_position);
|
||||
let drag_position = if state.hovered() {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
.or_else(|| {
|
||||
cx.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.map(|(drag_position, _)| drag_position)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Stack::new()
|
||||
.with_child(render_child(state, cx))
|
||||
@@ -70,10 +80,14 @@ where
|
||||
}
|
||||
})
|
||||
.on_move(|_, cx| {
|
||||
if cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
|
||||
if drag_and_drop
|
||||
.currently_dragged::<DraggedItem>(cx.window_id())
|
||||
.is_some()
|
||||
|| drag_and_drop
|
||||
.currently_dragged::<ProjectEntryId>(cx.window_id())
|
||||
.is_some()
|
||||
{
|
||||
cx.notify();
|
||||
} else {
|
||||
@@ -90,30 +104,60 @@ pub fn handle_dropped_item(
|
||||
split_margin: Option<f32>,
|
||||
cx: &mut EventContext,
|
||||
) {
|
||||
if let Some((_, dragged_item)) = cx
|
||||
.global::<DragAndDrop<Workspace>>()
|
||||
.currently_dragged::<DraggedItem>(cx.window_id)
|
||||
enum Action {
|
||||
Move(WeakViewHandle<Pane>, usize),
|
||||
Open(ProjectEntryId),
|
||||
}
|
||||
let drag_and_drop = cx.global::<DragAndDrop<Workspace>>();
|
||||
let action = if let Some((_, dragged_item)) =
|
||||
drag_and_drop.currently_dragged::<DraggedItem>(cx.window_id)
|
||||
{
|
||||
if let Some(split_direction) = split_margin
|
||||
.and_then(|margin| drop_split_direction(event.position, event.region, margin))
|
||||
{
|
||||
cx.dispatch_action(SplitWithItem {
|
||||
from: dragged_item.pane.clone(),
|
||||
item_id_to_move: dragged_item.item.id(),
|
||||
pane_to_split: pane.clone(),
|
||||
split_direction,
|
||||
});
|
||||
} else if pane != &dragged_item.pane || allow_same_pane {
|
||||
// If no split margin or not close enough to the edge, just move the item
|
||||
cx.dispatch_action(MoveItem {
|
||||
item_id: dragged_item.item.id(),
|
||||
from: dragged_item.pane.clone(),
|
||||
to: pane.clone(),
|
||||
destination_index: index,
|
||||
})
|
||||
}
|
||||
Action::Move(dragged_item.pane.clone(), dragged_item.item.id())
|
||||
} else if let Some((_, project_entry)) =
|
||||
drag_and_drop.currently_dragged::<ProjectEntryId>(cx.window_id)
|
||||
{
|
||||
Action::Open(*project_entry)
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(split_direction) =
|
||||
split_margin.and_then(|margin| drop_split_direction(event.position, event.region, margin))
|
||||
{
|
||||
let pane_to_split = pane.clone();
|
||||
match action {
|
||||
Action::Move(from, item_id_to_move) => cx.dispatch_action(SplitWithItem {
|
||||
from,
|
||||
item_id_to_move,
|
||||
pane_to_split,
|
||||
split_direction,
|
||||
}),
|
||||
Action::Open(project_entry) => cx.dispatch_action(SplitWithProjectEntry {
|
||||
pane_to_split,
|
||||
split_direction,
|
||||
project_entry,
|
||||
}),
|
||||
};
|
||||
} else {
|
||||
match action {
|
||||
Action::Move(from, item_id) => {
|
||||
if pane != &from || allow_same_pane {
|
||||
cx.dispatch_action(MoveItem {
|
||||
item_id,
|
||||
from,
|
||||
to: pane.clone(),
|
||||
destination_index: index,
|
||||
})
|
||||
} else {
|
||||
cx.propagate_event();
|
||||
}
|
||||
}
|
||||
Action::Open(project_entry) => cx.dispatch_action(OpenProjectEntryInPane {
|
||||
pane: pane.clone(),
|
||||
project_entry,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,12 +128,25 @@ pub struct OpenSharedScreen {
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SplitWithItem {
|
||||
from: WeakViewHandle<Pane>,
|
||||
pane_to_split: WeakViewHandle<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
from: WeakViewHandle<Pane>,
|
||||
item_id_to_move: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct SplitWithProjectEntry {
|
||||
pane_to_split: WeakViewHandle<Pane>,
|
||||
split_direction: SplitDirection,
|
||||
project_entry: ProjectEntryId,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct OpenProjectEntryInPane {
|
||||
pane: WeakViewHandle<Pane>,
|
||||
project_entry: ProjectEntryId,
|
||||
}
|
||||
|
||||
impl_internal_actions!(
|
||||
workspace,
|
||||
[
|
||||
@@ -143,6 +156,8 @@ impl_internal_actions!(
|
||||
OpenSharedScreen,
|
||||
RemoveWorktreeFromProject,
|
||||
SplitWithItem,
|
||||
SplitWithProjectEntry,
|
||||
OpenProjectEntryInPane,
|
||||
]
|
||||
);
|
||||
impl_actions!(workspace, [ActivatePane]);
|
||||
@@ -234,6 +249,57 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_async_action(
|
||||
|workspace: &mut Workspace,
|
||||
SplitWithProjectEntry {
|
||||
pane_to_split,
|
||||
split_direction,
|
||||
project_entry,
|
||||
}: &_,
|
||||
cx| {
|
||||
pane_to_split.upgrade(cx).and_then(|pane_to_split| {
|
||||
let new_pane = workspace.add_pane(cx);
|
||||
workspace
|
||||
.center
|
||||
.split(&pane_to_split, &new_pane, *split_direction)
|
||||
.unwrap();
|
||||
|
||||
workspace
|
||||
.project
|
||||
.read(cx)
|
||||
.path_for_entry(*project_entry, cx)
|
||||
.map(|path| {
|
||||
let task = workspace.open_path(path, Some(new_pane.downgrade()), true, cx);
|
||||
cx.foreground().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_async_action(
|
||||
|workspace: &mut Workspace,
|
||||
OpenProjectEntryInPane {
|
||||
pane,
|
||||
project_entry,
|
||||
}: &_,
|
||||
cx| {
|
||||
workspace
|
||||
.project
|
||||
.read(cx)
|
||||
.path_for_entry(*project_entry, cx)
|
||||
.map(|path| {
|
||||
let task = workspace.open_path(path, Some(pane.clone()), true, cx);
|
||||
cx.foreground().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let client = &app_state.client;
|
||||
client.add_view_request_handler(Workspace::handle_follow);
|
||||
client.add_view_message_handler(Workspace::handle_unfollow);
|
||||
@@ -1399,7 +1465,7 @@ impl Workspace {
|
||||
mut abs_paths: Vec<PathBuf>,
|
||||
visible: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>> {
|
||||
) -> Task<Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>> {
|
||||
let fs = self.fs.clone();
|
||||
|
||||
// Sort the paths to ensure we add worktrees for parents before their children.
|
||||
@@ -1429,7 +1495,7 @@ impl Workspace {
|
||||
if fs.is_file(&abs_path).await {
|
||||
Some(
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.open_path(project_path, true, cx)
|
||||
this.open_path(project_path, None, true, cx)
|
||||
})
|
||||
.await,
|
||||
)
|
||||
@@ -1749,10 +1815,11 @@ impl Workspace {
|
||||
pub fn open_path(
|
||||
&mut self,
|
||||
path: impl Into<ProjectPath>,
|
||||
pane: Option<WeakViewHandle<Pane>>,
|
||||
focus_item: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
|
||||
let pane = self.active_pane().downgrade();
|
||||
) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
|
||||
let pane = pane.unwrap_or_else(|| self.active_pane().downgrade());
|
||||
let task = self.load_path(path.into(), cx);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (project_entry_id, build_item) = task.await?;
|
||||
@@ -2874,7 +2941,7 @@ pub fn open_paths(
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<(
|
||||
ViewHandle<Workspace>,
|
||||
Vec<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
|
||||
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
|
||||
)> {
|
||||
log::info!("open paths {:?}", abs_paths);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.64.0"
|
||||
version = "0.65.0"
|
||||
|
||||
[lib]
|
||||
name = "zed"
|
||||
@@ -95,6 +95,7 @@ tree-sitter-c = "0.20.1"
|
||||
tree-sitter-cpp = "0.20.0"
|
||||
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
|
||||
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
|
||||
tree-sitter-embedded-template = "0.20.0"
|
||||
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
|
||||
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
|
||||
tree-sitter-rust = "0.20.3"
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -16,6 +16,9 @@ fn main() {
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path");
|
||||
}
|
||||
|
||||
// Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+.
|
||||
println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit");
|
||||
|
||||
// Seems to be required to enable Swift concurrency
|
||||
println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift");
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ mod installation;
|
||||
mod json;
|
||||
mod language_plugin;
|
||||
mod python;
|
||||
mod ruby;
|
||||
mod rust;
|
||||
mod typescript;
|
||||
|
||||
@@ -116,7 +117,16 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
|
||||
tree_sitter_html::language(),
|
||||
Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
|
||||
),
|
||||
("ruby", tree_sitter_ruby::language(), None),
|
||||
(
|
||||
"ruby",
|
||||
tree_sitter_ruby::language(),
|
||||
Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
|
||||
),
|
||||
(
|
||||
"erb",
|
||||
tree_sitter_embedded_template::language(),
|
||||
Some(CachedLspAdapter::new(ruby::RubyLanguageServer).await),
|
||||
),
|
||||
] {
|
||||
languages.add(language(name, grammar, lsp_adapter));
|
||||
}
|
||||
|
||||
8
crates/zed/src/languages/erb/config.toml
Normal file
8
crates/zed/src/languages/erb/config.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "ERB"
|
||||
path_suffixes = ["erb"]
|
||||
autoclose_before = ">})"
|
||||
brackets = [
|
||||
{ start = "<", end = ">", close = true, newline = true },
|
||||
]
|
||||
|
||||
block_comment = ["<%#", "%>"]
|
||||
12
crates/zed/src/languages/erb/highlights.scm
Normal file
12
crates/zed/src/languages/erb/highlights.scm
Normal file
@@ -0,0 +1,12 @@
|
||||
(comment_directive) @comment
|
||||
|
||||
[
|
||||
"<%#"
|
||||
"<%"
|
||||
"<%="
|
||||
"<%_"
|
||||
"<%-"
|
||||
"%>"
|
||||
"-%>"
|
||||
"_%>"
|
||||
] @keyword
|
||||
7
crates/zed/src/languages/erb/injections.scm
Normal file
7
crates/zed/src/languages/erb/injections.scm
Normal file
@@ -0,0 +1,7 @@
|
||||
((code) @content
|
||||
(#set! "language" "ruby")
|
||||
(#set! "combined"))
|
||||
|
||||
((content) @content
|
||||
(#set! "language" "html")
|
||||
(#set! "combined"))
|
||||
145
crates/zed/src/languages/ruby.rs
Normal file
145
crates/zed/src/languages/ruby.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use client::http::HttpClient;
|
||||
use language::{LanguageServerName, LspAdapter};
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
|
||||
pub struct RubyLanguageServer;
|
||||
|
||||
#[async_trait]
|
||||
impl LspAdapter for RubyLanguageServer {
|
||||
async fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("solargraph".into())
|
||||
}
|
||||
|
||||
async fn server_args(&self) -> Vec<String> {
|
||||
vec!["stdio".into()]
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: Arc<dyn HttpClient>,
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(()))
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
&self,
|
||||
_version: Box<dyn 'static + Send + Any>,
|
||||
_: Arc<dyn HttpClient>,
|
||||
_container_dir: PathBuf,
|
||||
) -> Result<PathBuf> {
|
||||
Err(anyhow!("solargraph must be installed manually"))
|
||||
}
|
||||
|
||||
async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option<PathBuf> {
|
||||
Some("solargraph".into())
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let label = &item.label;
|
||||
let grammar = language.grammar()?;
|
||||
let highlight_id = match item.kind? {
|
||||
lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
|
||||
lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
|
||||
lsp::CompletionItemKind::CLASS | lsp::CompletionItemKind::MODULE => {
|
||||
grammar.highlight_id_for_name("type")?
|
||||
}
|
||||
lsp::CompletionItemKind::KEYWORD => {
|
||||
if label.starts_with(":") {
|
||||
grammar.highlight_id_for_name("string.special.symbol")?
|
||||
} else {
|
||||
grammar.highlight_id_for_name("keyword")?
|
||||
}
|
||||
}
|
||||
lsp::CompletionItemKind::VARIABLE => {
|
||||
if label.starts_with("@") {
|
||||
grammar.highlight_id_for_name("property")?
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(language::CodeLabel {
|
||||
text: label.clone(),
|
||||
runs: vec![(0..label.len(), highlight_id)],
|
||||
filter_range: 0..label.len(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn label_for_symbol(
|
||||
&self,
|
||||
label: &str,
|
||||
kind: lsp::SymbolKind,
|
||||
language: &Arc<language::Language>,
|
||||
) -> Option<language::CodeLabel> {
|
||||
let grammar = language.grammar()?;
|
||||
match kind {
|
||||
lsp::SymbolKind::METHOD => {
|
||||
let mut parts = label.split('#');
|
||||
let classes = parts.next()?;
|
||||
let method = parts.next()?;
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let class_id = grammar.highlight_id_for_name("type")?;
|
||||
let method_id = grammar.highlight_id_for_name("function.method")?;
|
||||
|
||||
let mut ix = 0;
|
||||
let mut runs = Vec::new();
|
||||
for (i, class) in classes.split("::").enumerate() {
|
||||
if i > 0 {
|
||||
ix += 2;
|
||||
}
|
||||
let end_ix = ix + class.len();
|
||||
runs.push((ix..end_ix, class_id));
|
||||
ix = end_ix;
|
||||
}
|
||||
|
||||
ix += 1;
|
||||
let end_ix = ix + method.len();
|
||||
runs.push((ix..end_ix, method_id));
|
||||
Some(language::CodeLabel {
|
||||
text: label.to_string(),
|
||||
runs,
|
||||
filter_range: 0..label.len(),
|
||||
})
|
||||
}
|
||||
lsp::SymbolKind::CONSTANT => {
|
||||
let constant_id = grammar.highlight_id_for_name("constant")?;
|
||||
Some(language::CodeLabel {
|
||||
text: label.to_string(),
|
||||
runs: vec![(0..label.len(), constant_id)],
|
||||
filter_range: 0..label.len(),
|
||||
})
|
||||
}
|
||||
lsp::SymbolKind::CLASS | lsp::SymbolKind::MODULE => {
|
||||
let class_id = grammar.highlight_id_for_name("type")?;
|
||||
|
||||
let mut ix = 0;
|
||||
let mut runs = Vec::new();
|
||||
for (i, class) in label.split("::").enumerate() {
|
||||
if i > 0 {
|
||||
ix += "::".len();
|
||||
}
|
||||
let end_ix = ix + class.len();
|
||||
runs.push((ix..end_ix, class_id));
|
||||
ix = end_ix;
|
||||
}
|
||||
|
||||
Some(language::CodeLabel {
|
||||
text: label.to_string(),
|
||||
runs,
|
||||
filter_range: 0..label.len(),
|
||||
})
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,6 @@ fn main() {
|
||||
|
||||
context_menu::init(cx);
|
||||
project::Project::init(&client);
|
||||
client::Channel::init(&client);
|
||||
client::init(client.clone(), cx);
|
||||
command_palette::init(cx);
|
||||
editor::init(cx);
|
||||
|
||||
@@ -818,7 +818,7 @@ mod tests {
|
||||
|
||||
// Open the first entry
|
||||
let entry_1 = workspace
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| {
|
||||
@@ -832,7 +832,7 @@ mod tests {
|
||||
|
||||
// Open the second entry
|
||||
workspace
|
||||
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.read(|cx| {
|
||||
@@ -846,7 +846,7 @@ mod tests {
|
||||
|
||||
// Open the first entry again. The existing pane item is activated.
|
||||
let entry_1b = workspace
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(entry_1.id(), entry_1b.id());
|
||||
@@ -864,7 +864,7 @@ mod tests {
|
||||
workspace
|
||||
.update(cx, |w, cx| {
|
||||
w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
|
||||
w.open_path(file2.clone(), true, cx)
|
||||
w.open_path(file2.clone(), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -883,8 +883,8 @@ mod tests {
|
||||
// Open the third entry twice concurrently. Only one pane item is added.
|
||||
let (t1, t2) = workspace.update(cx, |w, cx| {
|
||||
(
|
||||
w.open_path(file3.clone(), true, cx),
|
||||
w.open_path(file3.clone(), true, cx),
|
||||
w.open_path(file3.clone(), None, true, cx),
|
||||
w.open_path(file3.clone(), None, true, cx),
|
||||
)
|
||||
});
|
||||
t1.await.unwrap();
|
||||
@@ -1195,7 +1195,7 @@ mod tests {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), true, cx)
|
||||
workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1284,7 +1284,7 @@ mod tests {
|
||||
let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
|
||||
|
||||
workspace
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1359,24 +1359,24 @@ mod tests {
|
||||
let file3 = entries[2].clone();
|
||||
|
||||
let editor1 = workspace
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
editor1.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
|
||||
});
|
||||
});
|
||||
let editor2 = workspace
|
||||
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let editor3 = workspace
|
||||
.update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
@@ -1384,7 +1384,7 @@ mod tests {
|
||||
|
||||
editor3
|
||||
.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::Fit), cx, |s| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
|
||||
});
|
||||
editor.newline(&Default::default(), cx);
|
||||
@@ -1626,22 +1626,22 @@ mod tests {
|
||||
let file4 = entries[3].clone();
|
||||
|
||||
let file1_item_id = workspace
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.id();
|
||||
let file2_item_id = workspace
|
||||
.update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.id();
|
||||
let file3_item_id = workspace
|
||||
.update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.id();
|
||||
let file4_item_id = workspace
|
||||
.update(cx, |w, cx| w.open_path(file4.clone(), true, cx))
|
||||
.update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.id();
|
||||
|
||||
@@ -7,7 +7,7 @@ echo "creating database..."
|
||||
script/sqlx database create
|
||||
|
||||
echo "migrating database..."
|
||||
cargo run -p collab -- migrate
|
||||
(cd crates/collab && cargo run -- migrate)
|
||||
|
||||
echo "seeding database..."
|
||||
script/seed-db
|
||||
|
||||
@@ -15,8 +15,6 @@ if [[ $(git rev-parse --abbrev-ref HEAD) != "main" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
git pull -q --ff-only origin main
|
||||
git fetch --tags
|
||||
cargo check -q
|
||||
|
||||
# Parse the current version
|
||||
version=$(script/get-crate-version zed)
|
||||
@@ -31,6 +29,10 @@ prev_minor_branch_name="v${major}.${prev_minor}.x"
|
||||
next_minor_branch_name="v${major}.${next_minor}.x"
|
||||
preview_tag_name="v${major}.${minor}.${patch}-pre"
|
||||
|
||||
git fetch origin ${prev_minor_branch_name}:${prev_minor_branch_name}
|
||||
git fetch origin --tags
|
||||
cargo check -q
|
||||
|
||||
function cleanup {
|
||||
git checkout -q main
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ export default function editor(colorScheme: ColorScheme) {
|
||||
}),
|
||||
},
|
||||
message: {
|
||||
text: text(layer, "sans", styleSet, "inverted", { size: "sm" }),
|
||||
highlightText: text(layer, "sans", styleSet, "inverted", {
|
||||
text: text(layer, "sans", styleSet, "default", { size: "sm" }),
|
||||
highlightText: text(layer, "sans", styleSet, "default", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
@@ -152,10 +152,10 @@ export default function editor(colorScheme: ColorScheme) {
|
||||
widthEm: 0.16,
|
||||
cornerRadius: 0.05,
|
||||
},
|
||||
documentHighlightReadBackground: colorScheme.ramps
|
||||
.neutral(0.5)
|
||||
.alpha(0.2)
|
||||
.hex(), // TODO: This was blend
|
||||
/** Highlights matching occurences of what is under the cursor
|
||||
* as well as matched brackets
|
||||
*/
|
||||
documentHighlightReadBackground: withOpacity(foreground(layer, "accent"), 0.1),
|
||||
documentHighlightWriteBackground: colorScheme.ramps
|
||||
.neutral(0.5)
|
||||
.alpha(0.4)
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, foreground, text } from "./components";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { background, border, foreground, text } from "./components";
|
||||
|
||||
export default function projectPanel(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle;
|
||||
|
||||
let entry = {
|
||||
|
||||
let baseEntry = {
|
||||
height: 24,
|
||||
iconColor: foreground(layer, "variant"),
|
||||
iconSize: 8,
|
||||
iconSpacing: 8,
|
||||
}
|
||||
|
||||
let entry = {
|
||||
...baseEntry,
|
||||
text: text(layer, "mono", "variant", { size: "sm" }),
|
||||
hover: {
|
||||
background: background(layer, "variant", "hovered"),
|
||||
@@ -28,6 +33,12 @@ export default function projectPanel(colorScheme: ColorScheme) {
|
||||
padding: { left: 12, right: 12, top: 6, bottom: 6 },
|
||||
indentWidth: 8,
|
||||
entry,
|
||||
draggedEntry: {
|
||||
...baseEntry,
|
||||
text: text(layer, "mono", "on", { size: "sm" }),
|
||||
background: withOpacity(background(layer, "on"), 0.9),
|
||||
border: border(layer),
|
||||
},
|
||||
ignoredEntry: {
|
||||
...entry,
|
||||
text: text(layer, "mono", "disabled"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ColorScheme } from "../themes/common/colorScheme";
|
||||
import { background, border, text } from "./components";
|
||||
import { withOpacity } from "../utils/color";
|
||||
import { background, border, foreground, text } from "./components";
|
||||
|
||||
export default function search(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest;
|
||||
@@ -26,7 +27,8 @@ export default function search(colorScheme: ColorScheme) {
|
||||
};
|
||||
|
||||
return {
|
||||
matchBackground: background(layer), // theme.editor.highlight.match,
|
||||
// TODO: Add an activeMatchBackground on the rust side to differenciate between active and inactive
|
||||
matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
|
||||
tabIconSpacing: 8,
|
||||
tabIconWidth: 14,
|
||||
optionButton: {
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function tabBar(colorScheme: ColorScheme) {
|
||||
|
||||
const draggedTab = {
|
||||
...activePaneActiveTab,
|
||||
background: withOpacity(tab.background, 0.95),
|
||||
background: withOpacity(tab.background, 0.9),
|
||||
border: undefined as any,
|
||||
shadow: colorScheme.popoverShadow,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user