mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Merge branch 'main' into piotr/z-2556-add-create-branch-button
This commit is contained in:
commit
2ac485a6ec
4
.github/workflows/release_actions.yml
vendored
4
.github/workflows/release_actions.yml
vendored
@ -16,8 +16,4 @@ jobs:
|
||||
|
||||
Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it.
|
||||
|
||||
```md
|
||||
# Changelog
|
||||
|
||||
${{ github.event.release.body }}
|
||||
```
|
||||
|
89
Cargo.lock
generated
89
Cargo.lock
generated
@ -482,7 +482,7 @@ dependencies = [
|
||||
"async-global-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"crossbeam-utils 0.8.15",
|
||||
"crossbeam-utils",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
@ -1491,6 +1491,7 @@ dependencies = [
|
||||
"theme",
|
||||
"theme_selector",
|
||||
"util",
|
||||
"vcs_menu",
|
||||
"workspace",
|
||||
"zed-actions",
|
||||
]
|
||||
@ -1550,7 +1551,7 @@ version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.8.15",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1863,16 +1864,6 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
|
||||
dependencies = [
|
||||
"crossbeam-utils 0.7.2",
|
||||
"maybe-uninit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.8"
|
||||
@ -1880,7 +1871,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.15",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1891,7 +1882,7 @@ checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils 0.8.15",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1902,7 +1893,7 @@ checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.15",
|
||||
"crossbeam-utils",
|
||||
"memoffset 0.8.0",
|
||||
"scopeguard",
|
||||
]
|
||||
@ -1914,18 +1905,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"crossbeam-utils 0.8.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
|
||||
dependencies = [
|
||||
"autocfg 1.1.0",
|
||||
"cfg-if 0.1.10",
|
||||
"lazy_static",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1990,7 +1970,6 @@ checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"libnghttp2-sys",
|
||||
"libz-sys",
|
||||
"openssl-sys",
|
||||
"pkg-config",
|
||||
@ -3521,12 +3500,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ipc-channel"
|
||||
version = "0.16.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb1d9211085f0ea6f1379d944b93c4d07e8207aa3bcf49f37eda12b85081887"
|
||||
checksum = "342d636452fbc2895574e0b319b23c014fd01c9ed71dcd87f6a4a8e2f948db4b"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"crossbeam-channel 0.4.4",
|
||||
"crossbeam-channel",
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
@ -3534,7 +3513,7 @@ dependencies = [
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"uuid 0.8.2",
|
||||
"uuid 1.3.2",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
@ -3576,7 +3555,7 @@ checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"castaway",
|
||||
"crossbeam-utils 0.8.15",
|
||||
"crossbeam-utils",
|
||||
"curl",
|
||||
"curl-sys",
|
||||
"encoding_rs",
|
||||
@ -3906,16 +3885,6 @@ version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
|
||||
|
||||
[[package]]
|
||||
name = "libnghttp2-sys"
|
||||
version = "0.1.7+1.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.24.2"
|
||||
@ -4004,7 +3973,6 @@ dependencies = [
|
||||
"gpui",
|
||||
"hmac 0.12.1",
|
||||
"jwt",
|
||||
"lazy_static",
|
||||
"live_kit_server",
|
||||
"log",
|
||||
"media",
|
||||
@ -4149,12 +4117,6 @@ version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-uninit"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.5"
|
||||
@ -5678,9 +5640,9 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
|
||||
dependencies = [
|
||||
"crossbeam-channel 0.5.8",
|
||||
"crossbeam-channel",
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils 0.8.15",
|
||||
"crossbeam-utils",
|
||||
"num_cpus",
|
||||
]
|
||||
|
||||
@ -8333,15 +8295,6 @@ version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
|
||||
dependencies = [
|
||||
"getrandom 0.2.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.3.2"
|
||||
@ -8378,6 +8331,19 @@ version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "vcs_menu"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"picker",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
@ -8398,7 +8364,6 @@ dependencies = [
|
||||
"indoc",
|
||||
"itertools",
|
||||
"language",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"nvim-rs",
|
||||
"parking_lot 0.11.2",
|
||||
|
@ -64,6 +64,7 @@ members = [
|
||||
"crates/theme_selector",
|
||||
"crates/util",
|
||||
"crates/vim",
|
||||
"crates/vcs_menu",
|
||||
"crates/workspace",
|
||||
"crates/welcome",
|
||||
"crates/xtask",
|
||||
@ -81,7 +82,8 @@ env_logger = { version = "0.9" }
|
||||
futures = { version = "0.3" }
|
||||
globset = { version = "0.4" }
|
||||
indoc = "1"
|
||||
isahc = "1.7.2"
|
||||
# We explicitly disable a http2 support in isahc.
|
||||
isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] }
|
||||
lazy_static = { version = "1.4.0" }
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
ordered-float = { version = "2.1.1" }
|
||||
|
4
assets/icons/radix/maximize.svg
Normal file
4
assets/icons/radix/maximize.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.5 1.5H13.5M13.5 1.5V5.5M13.5 1.5C12.1332 2.86683 10.3668 4.63317 9 6" stroke="white" stroke-linecap="round"/>
|
||||
<path d="M1.5 9.5V13.5M1.5 13.5L6 9M1.5 13.5H5.5" stroke="white" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 315 B |
4
assets/icons/radix/minimize.svg
Normal file
4
assets/icons/radix/minimize.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 6L9 6M9 6L9 2M9 6C10.3668 4.63316 12.1332 2.86683 13.5 1.5" stroke="white" stroke-linecap="round"/>
|
||||
<path d="M6 13L6 9M6 9L1.5 13.5M6 9L2 9" stroke="white" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 297 B |
@ -39,6 +39,7 @@
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open",
|
||||
"alt-cmd-o": "projects::OpenRecent",
|
||||
"alt-cmd-b": "branches::OpenRecent",
|
||||
"ctrl-~": "workspace::NewTerminal",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"shift-escape": "workspace::ToggleZoom"
|
||||
|
@ -2,6 +2,7 @@
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-shift-o": "projects::OpenRecent",
|
||||
"cmd-shift-b": "branches::OpenRecent",
|
||||
"cmd-alt-tab": "project_panel::ToggleFocus"
|
||||
}
|
||||
},
|
||||
|
@ -35,8 +35,11 @@
|
||||
"l": "vim::Right",
|
||||
"right": "vim::Right",
|
||||
"$": "vim::EndOfLine",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
"shift-g": "vim::EndOfDocument",
|
||||
"w": "vim::NextWordStart",
|
||||
"{": "vim::StartOfParagraph",
|
||||
"}": "vim::EndOfParagraph",
|
||||
"shift-w": [
|
||||
"vim::NextWordStart",
|
||||
{
|
||||
@ -92,7 +95,10 @@
|
||||
],
|
||||
"ctrl-o": "pane::GoBack",
|
||||
"ctrl-]": "editor::GoToDefinition",
|
||||
"escape": "editor::Cancel",
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||
"1": [
|
||||
"vim::Number",
|
||||
@ -165,7 +171,6 @@
|
||||
"shift-a": "vim::InsertEndOfLine",
|
||||
"x": "vim::DeleteRight",
|
||||
"shift-x": "vim::DeleteLeft",
|
||||
"^": "vim::FirstNonWhitespace",
|
||||
"o": "vim::InsertLineBelow",
|
||||
"shift-o": "vim::InsertLineAbove",
|
||||
"~": "vim::ChangeCase",
|
||||
@ -305,6 +310,10 @@
|
||||
"vim::PushOperator",
|
||||
"Replace"
|
||||
],
|
||||
"ctrl-c": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
],
|
||||
"> >": "editor::Indent",
|
||||
"< <": "editor::Outdent"
|
||||
}
|
||||
@ -321,7 +330,10 @@
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": "editor::Cancel"
|
||||
"escape": [
|
||||
"vim::SwitchMode",
|
||||
"Normal"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -2061,6 +2061,8 @@ impl ConversationEditor {
|
||||
let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
|
||||
let remaining_tokens_style = if remaining_tokens <= 0 {
|
||||
&style.no_remaining_tokens
|
||||
} else if remaining_tokens <= 500 {
|
||||
&style.low_remaining_tokens
|
||||
} else {
|
||||
&style.remaining_tokens
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ pub mod room;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{proto, Client, TypedEnvelope, User, UserStore};
|
||||
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
|
||||
use collections::HashSet;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use postage::watch;
|
||||
@ -198,6 +198,7 @@ impl ActiveCall {
|
||||
let result = invite.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_invites.remove(&called_user_id);
|
||||
this.report_call_event("invite", cx);
|
||||
cx.notify();
|
||||
});
|
||||
result
|
||||
@ -243,21 +244,26 @@ impl ActiveCall {
|
||||
};
|
||||
|
||||
let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let room = join.await?;
|
||||
this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx))
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.report_call_event("accept incoming", cx)
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decline_incoming(&mut self) -> Result<()> {
|
||||
pub fn decline_incoming(&mut self, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
let call = self
|
||||
.incoming_call
|
||||
.0
|
||||
.borrow_mut()
|
||||
.take()
|
||||
.ok_or_else(|| anyhow!("no incoming call"))?;
|
||||
self.report_call_event_for_room("decline incoming", call.room_id, cx);
|
||||
self.client.send(proto::DeclineCall {
|
||||
room_id: call.room_id,
|
||||
})?;
|
||||
@ -266,6 +272,7 @@ impl ActiveCall {
|
||||
|
||||
pub fn hang_up(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
cx.notify();
|
||||
self.report_call_event("hang up", cx);
|
||||
if let Some((room, _)) = self.room.take() {
|
||||
room.update(cx, |room, cx| room.leave(cx))
|
||||
} else {
|
||||
@ -273,12 +280,28 @@ impl ActiveCall {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_screen_sharing(&self, cx: &mut AppContext) {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
self.report_call_event("disable screen share", cx);
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
self.report_call_event("enable screen share", cx);
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn share_project(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<u64>> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("share project", cx);
|
||||
room.update(cx, |room, cx| room.share_project(project, cx))
|
||||
} else {
|
||||
Task::ready(Err(anyhow!("no active call")))
|
||||
@ -291,6 +314,7 @@ impl ActiveCall {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
if let Some((room, _)) = self.room.as_ref() {
|
||||
self.report_call_event("unshare project", cx);
|
||||
room.update(cx, |room, cx| room.unshare_project(project, cx))
|
||||
} else {
|
||||
Err(anyhow!("no active call"))
|
||||
@ -352,4 +376,19 @@ impl ActiveCall {
|
||||
pub fn pending_invites(&self) -> &HashSet<u64> {
|
||||
&self.pending_invites
|
||||
}
|
||||
|
||||
fn report_call_event(&self, operation: &'static str, cx: &AppContext) {
|
||||
if let Some(room) = self.room() {
|
||||
self.report_call_event_for_room(operation, room.read(cx).id(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn report_call_event_for_room(&self, operation: &'static str, room_id: u64, cx: &AppContext) {
|
||||
let telemetry = self.client.telemetry();
|
||||
let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
|
||||
|
||||
let event = ClickhouseEvent::Call { operation, room_id };
|
||||
|
||||
telemetry.report_clickhouse_event(event, telemetry_settings);
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +201,7 @@ impl Bundle {
|
||||
self.zed_version_string()
|
||||
);
|
||||
}
|
||||
|
||||
Self::LocalPath { executable, .. } => {
|
||||
let executable_parent = executable
|
||||
.parent()
|
||||
|
@ -70,6 +70,10 @@ pub enum ClickhouseEvent {
|
||||
suggestion_accepted: bool,
|
||||
file_extension: Option<String>,
|
||||
},
|
||||
Call {
|
||||
operation: &'static str,
|
||||
room_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -3517,7 +3517,6 @@ pub use test::*;
|
||||
mod test {
|
||||
use super::*;
|
||||
use gpui::executor::Background;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use sea_orm::ConnectionTrait;
|
||||
use sqlx::migrate::MigrateDatabase;
|
||||
@ -3566,9 +3565,7 @@ mod test {
|
||||
}
|
||||
|
||||
pub fn postgres(background: Arc<Background>) -> Self {
|
||||
lazy_static! {
|
||||
static ref LOCK: Mutex<()> = Mutex::new(());
|
||||
}
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
let _guard = LOCK.lock();
|
||||
let mut rng = StdRng::from_entropy();
|
||||
|
@ -157,7 +157,7 @@ async fn test_basic_calls(
|
||||
// User C receives the call, but declines it.
|
||||
let call_c = incoming_call_c.next().await.unwrap().unwrap();
|
||||
assert_eq!(call_c.calling_user.github_login, "user_b");
|
||||
active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap());
|
||||
active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap());
|
||||
assert!(incoming_call_c.next().await.unwrap().is_none());
|
||||
|
||||
deterministic.run_until_parked();
|
||||
@ -1080,7 +1080,7 @@ async fn test_calls_on_multiple_connections(
|
||||
|
||||
// User B declines the call on one of the two connections, causing both connections
|
||||
// to stop ringing.
|
||||
active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap());
|
||||
active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap());
|
||||
deterministic.run_until_parked();
|
||||
assert!(incoming_call_b1.next().await.unwrap().is_none());
|
||||
assert!(incoming_call_b2.next().await.unwrap().is_none());
|
||||
@ -5945,7 +5945,7 @@ async fn test_contacts(
|
||||
[("user_b".to_string(), "online", "busy")]
|
||||
);
|
||||
|
||||
active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap());
|
||||
active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap());
|
||||
deterministic.run_until_parked();
|
||||
assert_eq!(
|
||||
contacts(&client_a, cx_a),
|
||||
|
@ -37,9 +37,9 @@ use util::ResultExt;
|
||||
lazy_static::lazy_static! {
|
||||
static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
|
||||
static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
|
||||
static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
|
||||
static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
|
||||
}
|
||||
static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
||||
static PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Mutex::new(None);
|
||||
|
||||
#[gpui::test(iterations = 100, on_failure = "on_failure")]
|
||||
async fn test_random_collaboration(
|
||||
@ -365,7 +365,7 @@ async fn apply_client_operation(
|
||||
}
|
||||
|
||||
log::info!("{}: declining incoming call", client.username);
|
||||
active_call.update(cx, |call, _| call.decline_incoming())?;
|
||||
active_call.update(cx, |call, cx| call.decline_incoming(cx))?;
|
||||
}
|
||||
|
||||
ClientOperation::LeaveCall => {
|
||||
|
@ -39,6 +39,7 @@ recent_projects = {path = "../recent_projects"}
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
theme_selector = { path = "../theme_selector" }
|
||||
vcs_menu = { path = "../vcs_menu" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
zed-actions = {path = "../zed-actions"}
|
||||
|
@ -1,8 +1,5 @@
|
||||
use crate::{
|
||||
branch_list::{build_branch_list, BranchList},
|
||||
contact_notification::ContactNotification,
|
||||
contacts_popover,
|
||||
face_pile::FacePile,
|
||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
||||
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
|
||||
ToggleScreenSharing,
|
||||
};
|
||||
@ -27,6 +24,7 @@ use recent_projects::{build_recent_projects, RecentProjects};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use theme::{AvatarStyle, Theme};
|
||||
use util::ResultExt;
|
||||
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
|
||||
use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
|
||||
|
||||
const MAX_PROJECT_NAME_LENGTH: usize = 40;
|
||||
@ -37,7 +35,6 @@ actions!(
|
||||
[
|
||||
ToggleContactsMenu,
|
||||
ToggleUserMenu,
|
||||
ToggleVcsMenu,
|
||||
ToggleProjectMenu,
|
||||
SwitchBranch,
|
||||
ShareProject,
|
||||
@ -229,15 +226,23 @@ impl CollabTitlebarItem {
|
||||
let mut ret = Flex::row().with_child(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
|
||||
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, cx| {
|
||||
let style = project_style
|
||||
.in_state(self.project_popover.is_some())
|
||||
.style_for(mouse_state);
|
||||
enum RecentProjectsTooltip {}
|
||||
Label::new(name, style.text.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.with_tooltip::<RecentProjectsTooltip>(
|
||||
0,
|
||||
"Recent projects".into(),
|
||||
Some(Box::new(recent_projects::OpenRecent)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any_named("title-project-name")
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
@ -264,7 +269,8 @@ impl CollabTitlebarItem {
|
||||
MouseEventHandler::<ToggleVcsMenu, Self>::new(
|
||||
0,
|
||||
cx,
|
||||
|mouse_state, _| {
|
||||
|mouse_state, cx| {
|
||||
enum BranchPopoverTooltip {}
|
||||
let style = git_style
|
||||
.in_state(self.branch_popover.is_some())
|
||||
.style_for(mouse_state);
|
||||
@ -274,6 +280,13 @@ impl CollabTitlebarItem {
|
||||
.with_margin_right(item_spacing)
|
||||
.aligned()
|
||||
.left()
|
||||
.with_tooltip::<BranchPopoverTooltip>(
|
||||
0,
|
||||
"Recent branches".into(),
|
||||
Some(Box::new(ToggleVcsMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.into_any_named("title-project-branch")
|
||||
},
|
||||
)
|
||||
|
@ -1,4 +1,3 @@
|
||||
mod branch_list;
|
||||
mod collab_titlebar_item;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
@ -12,7 +11,7 @@ mod sharing_status_indicator;
|
||||
|
||||
use call::{ActiveCall, Room};
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use gpui::{actions, AppContext, Task};
|
||||
use gpui::{actions, AppContext};
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
@ -29,7 +28,7 @@ actions!(
|
||||
);
|
||||
|
||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
branch_list::init(cx);
|
||||
vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
contact_list::init(cx);
|
||||
contact_finder::init(cx);
|
||||
@ -45,16 +44,9 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
|
||||
let toggle_screen_sharing = room.update(cx, |room, cx| {
|
||||
if room.is_screen_sharing() {
|
||||
Task::ready(room.unshare_screen(cx))
|
||||
} else {
|
||||
room.share_screen(cx)
|
||||
}
|
||||
});
|
||||
toggle_screen_sharing.detach_and_log_err(cx);
|
||||
}
|
||||
ActiveCall::global(cx).update(cx, |call, cx| {
|
||||
call.toggle_screen_sharing(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
||||
|
@ -99,8 +99,8 @@ impl IncomingCallNotification {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
} else {
|
||||
active_call.update(cx, |active_call, _| {
|
||||
active_call.decline_incoming().log_err();
|
||||
active_call.update(cx, |active_call, cx| {
|
||||
active_call.decline_incoming(cx).log_err();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -369,6 +369,7 @@ mod tests {
|
||||
editor::init(cx);
|
||||
workspace::init(app_state.clone(), cx);
|
||||
init(cx);
|
||||
Project::init_settings(cx);
|
||||
app_state
|
||||
})
|
||||
}
|
||||
|
@ -41,12 +41,11 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
|
||||
const DB_FILE_NAME: &'static str = "db.sqlite";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
|
||||
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||
static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||
pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
|
||||
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
|
||||
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
|
||||
}
|
||||
static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
|
||||
|
||||
/// Open or create a database at the given directory path.
|
||||
/// This will retry a couple times if there are failures. If opening fails once, the db directory
|
||||
|
@ -5123,7 +5123,7 @@ impl Editor {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::start_of_paragraph(map, selection.head()),
|
||||
movement::start_of_paragraph(map, selection.head(), 1),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
@ -5143,7 +5143,7 @@ impl Editor {
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
selection.collapse_to(
|
||||
movement::end_of_paragraph(map, selection.head()),
|
||||
movement::end_of_paragraph(map, selection.head(), 1),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
@ -5162,7 +5162,10 @@ impl Editor {
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::start_of_paragraph(map, head), SelectionGoal::None)
|
||||
(
|
||||
movement::start_of_paragraph(map, head, 1),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
@ -5179,7 +5182,10 @@ impl Editor {
|
||||
|
||||
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_heads_with(|map, head, _| {
|
||||
(movement::end_of_paragraph(map, head), SelectionGoal::None)
|
||||
(
|
||||
movement::end_of_paragraph(map, head, 1),
|
||||
SelectionGoal::None,
|
||||
)
|
||||
});
|
||||
})
|
||||
}
|
||||
@ -7216,6 +7222,47 @@ impl Editor {
|
||||
}
|
||||
results
|
||||
}
|
||||
pub fn background_highlights_in_range_for<T: 'static>(
|
||||
&self,
|
||||
search_range: Range<Anchor>,
|
||||
display_snapshot: &DisplaySnapshot,
|
||||
theme: &Theme,
|
||||
) -> Vec<(Range<DisplayPoint>, Color)> {
|
||||
let mut results = Vec::new();
|
||||
let buffer = &display_snapshot.buffer_snapshot;
|
||||
let Some((color_fetcher, ranges)) = self.background_highlights
|
||||
.get(&TypeId::of::<T>()) else {
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let color = color_fetcher(theme);
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = probe.end.cmp(&search_range.start, buffer);
|
||||
if cmp.is_gt() {
|
||||
Ordering::Greater
|
||||
} else {
|
||||
Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
if range.start.cmp(&search_range.end, buffer).is_ge() {
|
||||
break;
|
||||
}
|
||||
let start = range
|
||||
.start
|
||||
.to_point(buffer)
|
||||
.to_display_point(display_snapshot);
|
||||
let end = range
|
||||
.end
|
||||
.to_point(buffer)
|
||||
.to_display_point(display_snapshot);
|
||||
results.push((start..end, color))
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub fn highlight_text<T: 'static>(
|
||||
&mut self,
|
||||
@ -7518,7 +7565,7 @@ impl Editor {
|
||||
|
||||
fn report_editor_event(
|
||||
&self,
|
||||
name: &'static str,
|
||||
operation: &'static str,
|
||||
file_extension: Option<String>,
|
||||
cx: &AppContext,
|
||||
) {
|
||||
@ -7555,7 +7602,7 @@ impl Editor {
|
||||
let event = ClickhouseEvent::Editor {
|
||||
file_extension,
|
||||
vim_mode,
|
||||
operation: name,
|
||||
operation,
|
||||
copilot_enabled,
|
||||
copilot_enabled_for_language,
|
||||
};
|
||||
|
@ -22,7 +22,10 @@ use language::{
|
||||
BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
|
||||
use unindent::Unindent;
|
||||
use util::{
|
||||
@ -1796,7 +1799,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
|
||||
"});
|
||||
}
|
||||
// Ensure that comment continuations can be disabled.
|
||||
update_test_settings(cx, |settings| {
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.extend_comment_on_newline = Some(false);
|
||||
});
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
@ -4546,7 +4549,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_settings(cx, |settings| {
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
@ -4660,7 +4663,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
|
||||
assert!(!cx.read(|cx| editor.is_dirty(cx)));
|
||||
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_settings(cx, |settings| {
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
@ -7084,6 +7087,142 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let language_name: Arc<str> = "Rust".into();
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: Arc::clone(&language_name),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
|
||||
let server_restarts = Arc::new(AtomicUsize::new(0));
|
||||
let closure_restarts = Arc::clone(&server_restarts);
|
||||
let language_server_name = "test language server";
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
name: language_server_name,
|
||||
initialization_options: Some(json!({
|
||||
"testOptionValue": true
|
||||
})),
|
||||
initializer: Some(Box::new(move |fake_server| {
|
||||
let task_restarts = Arc::clone(&closure_restarts);
|
||||
fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
|
||||
task_restarts.fetch_add(1, atomic::Ordering::Release);
|
||||
futures::future::ready(Ok(()))
|
||||
});
|
||||
})),
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": "fn main() { let a = 5; }",
|
||||
"other.rs": "// Test file",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, ["/a".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
let (_, _workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/a/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let _fake_server = fake_servers.next().await.unwrap();
|
||||
update_test_language_settings(cx, |language_settings| {
|
||||
language_settings.languages.insert(
|
||||
Arc::clone(&language_name),
|
||||
LanguageSettingsContent {
|
||||
tab_size: NonZeroU32::new(8),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
server_restarts.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Should not restart LSP server on an unrelated change"
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
"Some other server name".into(),
|
||||
LspSettings {
|
||||
initialization_options: Some(json!({
|
||||
"some other init value": false
|
||||
})),
|
||||
},
|
||||
);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
server_restarts.load(atomic::Ordering::Acquire),
|
||||
0,
|
||||
"Should not restart LSP server on an unrelated LSP settings change"
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
initialization_options: Some(json!({
|
||||
"anotherInitValue": false
|
||||
})),
|
||||
},
|
||||
);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
server_restarts.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"Should restart LSP server on a related LSP settings change"
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
initialization_options: Some(json!({
|
||||
"anotherInitValue": false
|
||||
})),
|
||||
},
|
||||
);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
server_restarts.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"Should not restart LSP server on a related LSP settings change that is the same"
|
||||
);
|
||||
|
||||
update_test_project_settings(cx, |project_settings| {
|
||||
project_settings.lsp.insert(
|
||||
language_server_name.into(),
|
||||
LspSettings {
|
||||
initialization_options: None,
|
||||
},
|
||||
);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
server_restarts.load(atomic::Ordering::Acquire),
|
||||
2,
|
||||
"Should restart LSP server on another related LSP settings change"
|
||||
);
|
||||
}
|
||||
|
||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||
let point = DisplayPoint::new(row as u32, column as u32);
|
||||
point..point
|
||||
@ -7203,7 +7342,7 @@ fn handle_copilot_completion_request(
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_test_settings(
|
||||
pub(crate) fn update_test_language_settings(
|
||||
cx: &mut TestAppContext,
|
||||
f: impl Fn(&mut AllLanguageSettingsContent),
|
||||
) {
|
||||
@ -7214,6 +7353,17 @@ pub(crate) fn update_test_settings(
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_test_project_settings(
|
||||
cx: &mut TestAppContext,
|
||||
f: impl Fn(&mut ProjectSettings),
|
||||
) {
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||
store.update_user_settings::<ProjectSettings>(cx, f);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
@ -7227,5 +7377,5 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
|
||||
crate::init(cx);
|
||||
});
|
||||
|
||||
update_test_settings(cx, f);
|
||||
update_test_language_settings(cx, f);
|
||||
}
|
||||
|
@ -1086,11 +1086,13 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
};
|
||||
for (row, _) in &editor.background_highlights_in_range(
|
||||
start_anchor..end_anchor,
|
||||
&layout.position_map.snapshot,
|
||||
&theme,
|
||||
) {
|
||||
for (row, _) in &editor
|
||||
.background_highlights_in_range_for::<crate::items::BufferSearchHighlights>(
|
||||
start_anchor..end_anchor,
|
||||
&layout.position_map.snapshot,
|
||||
&theme,
|
||||
)
|
||||
{
|
||||
let start_display = row.start;
|
||||
let end_display = row.end;
|
||||
|
||||
@ -2149,6 +2151,9 @@ impl Element<Editor> for EditorElement {
|
||||
ShowScrollbar::Auto => {
|
||||
// Git
|
||||
(is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
|
||||
||
|
||||
// Selections
|
||||
(is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
|
||||
// Scrollmanager
|
||||
|| editor.scroll_manager.scrollbars_visible()
|
||||
}
|
||||
@ -2911,7 +2916,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{BlockDisposition, BlockProperties},
|
||||
editor_tests::{init_test, update_test_settings},
|
||||
editor_tests::{init_test, update_test_language_settings},
|
||||
Editor, MultiBuffer,
|
||||
};
|
||||
use gpui::TestAppContext;
|
||||
@ -3108,7 +3113,7 @@ mod tests {
|
||||
let resize_step = 10.0;
|
||||
let mut editor_width = 200.0;
|
||||
while editor_width <= 1000.0 {
|
||||
update_test_settings(cx, |s| {
|
||||
update_test_language_settings(cx, |s| {
|
||||
s.defaults.tab_size = NonZeroU32::new(tab_size);
|
||||
s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
|
||||
s.defaults.preferred_line_length = Some(editor_width as u32);
|
||||
|
@ -847,7 +847,7 @@ mod tests {
|
||||
use text::Point;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::editor_tests::update_test_settings;
|
||||
use crate::editor_tests::update_test_language_settings;
|
||||
|
||||
use super::*;
|
||||
|
||||
@ -1476,7 +1476,7 @@ mod tests {
|
||||
),
|
||||
] {
|
||||
edits_made += 1;
|
||||
update_test_settings(cx, |settings| {
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
|
||||
@ -1520,7 +1520,7 @@ mod tests {
|
||||
|
||||
edits_made += 1;
|
||||
let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
|
||||
update_test_settings(cx, |settings| {
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: false,
|
||||
show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
|
||||
@ -1577,7 +1577,7 @@ mod tests {
|
||||
|
||||
let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
|
||||
edits_made += 1;
|
||||
update_test_settings(cx, |settings| {
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
|
||||
@ -2269,7 +2269,7 @@ unedited (2nd) buffer should have the same hint");
|
||||
crate::init(cx);
|
||||
});
|
||||
|
||||
update_test_settings(cx, f);
|
||||
update_test_language_settings(cx, f);
|
||||
}
|
||||
|
||||
async fn prepare_test_objects(
|
||||
|
@ -883,7 +883,7 @@ impl ProjectItem for Editor {
|
||||
}
|
||||
}
|
||||
|
||||
enum BufferSearchHighlights {}
|
||||
pub(crate) enum BufferSearchHighlights {}
|
||||
impl SearchableItem for Editor {
|
||||
type Match = Range<Anchor>;
|
||||
|
||||
|
@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
||||
pub fn start_of_paragraph(
|
||||
map: &DisplaySnapshot,
|
||||
display_point: DisplayPoint,
|
||||
mut count: usize,
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == 0 {
|
||||
return map.max_point();
|
||||
@ -203,7 +207,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
|
||||
for row in (0..point.row + 1).rev() {
|
||||
let blank = map.buffer_snapshot.is_line_blank(row);
|
||||
if found_non_blank_line && blank {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
if count <= 1 {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
}
|
||||
count -= 1;
|
||||
found_non_blank_line = false;
|
||||
}
|
||||
|
||||
found_non_blank_line |= !blank;
|
||||
@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
|
||||
DisplayPoint::zero()
|
||||
}
|
||||
|
||||
pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
|
||||
pub fn end_of_paragraph(
|
||||
map: &DisplaySnapshot,
|
||||
display_point: DisplayPoint,
|
||||
mut count: usize,
|
||||
) -> DisplayPoint {
|
||||
let point = display_point.to_point(map);
|
||||
if point.row == map.max_buffer_row() {
|
||||
return DisplayPoint::zero();
|
||||
@ -222,7 +234,11 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D
|
||||
for row in point.row..map.max_buffer_row() + 1 {
|
||||
let blank = map.buffer_snapshot.is_line_blank(row);
|
||||
if found_non_blank_line && blank {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
if count <= 1 {
|
||||
return Point::new(row, 0).to_display_point(map);
|
||||
}
|
||||
count -= 1;
|
||||
found_non_blank_line = false;
|
||||
}
|
||||
|
||||
found_non_blank_line |= !blank;
|
||||
|
@ -210,6 +210,10 @@ impl<'a> EditorTestContext<'a> {
|
||||
self.assert_selections(expected_selections, marked_text.to_string())
|
||||
}
|
||||
|
||||
pub fn editor_state(&mut self) -> String {
|
||||
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
|
||||
let expected_ranges = self.ranges(marked_text);
|
||||
@ -248,14 +252,8 @@ impl<'a> EditorTestContext<'a> {
|
||||
self.assert_selections(expected_selections, expected_marked_text)
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_selections(
|
||||
&mut self,
|
||||
expected_selections: Vec<Range<usize>>,
|
||||
expected_marked_text: String,
|
||||
) {
|
||||
let actual_selections = self
|
||||
.editor
|
||||
fn editor_selections(&self) -> Vec<Range<usize>> {
|
||||
self.editor
|
||||
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
@ -265,12 +263,22 @@ impl<'a> EditorTestContext<'a> {
|
||||
s.start..s.end
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_selections(
|
||||
&mut self,
|
||||
expected_selections: Vec<Range<usize>>,
|
||||
expected_marked_text: String,
|
||||
) {
|
||||
let actual_selections = self.editor_selections();
|
||||
let actual_marked_text =
|
||||
generate_marked_text(&self.buffer_text(), &actual_selections, true);
|
||||
if expected_selections != actual_selections {
|
||||
panic!(
|
||||
indoc! {"
|
||||
|
||||
{}Editor has unexpected selections.
|
||||
|
||||
Expected selections:
|
||||
|
@ -427,6 +427,7 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeLspAdapter {
|
||||
pub name: &'static str,
|
||||
pub initialization_options: Option<Value>,
|
||||
pub capabilities: lsp::ServerCapabilities,
|
||||
pub initializer: Option<Box<dyn 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer)>>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
@ -1637,6 +1638,7 @@ impl Default for FakeLspAdapter {
|
||||
capabilities: lsp::LanguageServer::full_capabilities(),
|
||||
initializer: None,
|
||||
disk_based_diagnostics_progress_token: None,
|
||||
initialization_options: None,
|
||||
disk_based_diagnostics_sources: Vec::new(),
|
||||
}
|
||||
}
|
||||
@ -1686,6 +1688,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
|
||||
async fn disk_based_diagnostics_progress_token(&self) -> Option<String> {
|
||||
self.disk_based_diagnostics_progress_token.clone()
|
||||
}
|
||||
|
||||
async fn initialization_options(&self) -> Option<Value> {
|
||||
self.initialization_options.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {
|
||||
|
@ -4,7 +4,6 @@ mod syntax_map_tests;
|
||||
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
|
||||
use collections::HashMap;
|
||||
use futures::FutureExt;
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
@ -25,9 +24,7 @@ thread_local! {
|
||||
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
|
||||
}
|
||||
static QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Mutex::new(vec![]);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SyntaxMap {
|
||||
|
@ -17,7 +17,6 @@ test-support = [
|
||||
"async-trait",
|
||||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"lazy_static",
|
||||
"live_kit_server",
|
||||
"nanoid",
|
||||
]
|
||||
@ -38,7 +37,6 @@ parking_lot.workspace = true
|
||||
postage.workspace = true
|
||||
|
||||
async-trait = { workspace = true, optional = true }
|
||||
lazy_static = { workspace = true, optional = true }
|
||||
nanoid = { version ="0.4", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
@ -60,7 +58,6 @@ foreign-types = "0.3"
|
||||
futures.workspace = true
|
||||
hmac = "0.12"
|
||||
jwt = "0.16"
|
||||
lazy_static.workspace = true
|
||||
objc = "0.2"
|
||||
parking_lot.workspace = true
|
||||
serde.workspace = true
|
||||
|
@ -1,18 +1,15 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use collections::{BTreeMap, HashMap};
|
||||
use futures::Stream;
|
||||
use gpui::executor::Background;
|
||||
use lazy_static::lazy_static;
|
||||
use live_kit_server::token;
|
||||
use media::core_video::CVImageBuffer;
|
||||
use parking_lot::Mutex;
|
||||
use postage::watch;
|
||||
use std::{future::Future, mem, sync::Arc};
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default();
|
||||
}
|
||||
static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
|
||||
|
||||
pub struct TestServer {
|
||||
pub url: String,
|
||||
|
@ -50,7 +50,7 @@ use lsp::{
|
||||
};
|
||||
use lsp_command::*;
|
||||
use postage::watch;
|
||||
use project_settings::ProjectSettings;
|
||||
use project_settings::{LspSettings, ProjectSettings};
|
||||
use rand::prelude::*;
|
||||
use search::SearchQuery;
|
||||
use serde::Serialize;
|
||||
@ -149,6 +149,7 @@ pub struct Project {
|
||||
_maintain_workspace_config: Task<()>,
|
||||
terminals: Terminals,
|
||||
copilot_enabled: bool,
|
||||
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
||||
}
|
||||
|
||||
struct DelayedDebounced {
|
||||
@ -614,6 +615,7 @@ impl Project {
|
||||
local_handles: Vec::new(),
|
||||
},
|
||||
copilot_enabled: Copilot::global(cx).is_some(),
|
||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -706,6 +708,7 @@ impl Project {
|
||||
local_handles: Vec::new(),
|
||||
},
|
||||
copilot_enabled: Copilot::global(cx).is_some(),
|
||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||
};
|
||||
for worktree in worktrees {
|
||||
let _ = this.add_worktree(&worktree, cx);
|
||||
@ -779,7 +782,9 @@ impl Project {
|
||||
let mut language_servers_to_stop = Vec::new();
|
||||
let mut language_servers_to_restart = Vec::new();
|
||||
let languages = self.languages.to_vec();
|
||||
let project_settings = settings::get::<ProjectSettings>(cx).clone();
|
||||
|
||||
let new_lsp_settings = settings::get::<ProjectSettings>(cx).lsp.clone();
|
||||
let current_lsp_settings = &self.current_lsp_settings;
|
||||
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
|
||||
let language = languages.iter().find_map(|l| {
|
||||
let adapter = l
|
||||
@ -796,16 +801,25 @@ impl Project {
|
||||
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
|
||||
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
|
||||
} else if let Some(worktree) = worktree {
|
||||
let new_lsp_settings = project_settings
|
||||
.lsp
|
||||
.get(&adapter.name.0)
|
||||
.and_then(|s| s.initialization_options.as_ref());
|
||||
if adapter.initialization_options.as_ref() != new_lsp_settings {
|
||||
language_servers_to_restart.push((worktree, Arc::clone(language)));
|
||||
let server_name = &adapter.name.0;
|
||||
match (
|
||||
current_lsp_settings.get(server_name),
|
||||
new_lsp_settings.get(server_name),
|
||||
) {
|
||||
(None, None) => {}
|
||||
(Some(_), None) | (None, Some(_)) => {
|
||||
language_servers_to_restart.push((worktree, Arc::clone(language)));
|
||||
}
|
||||
(Some(current_lsp_settings), Some(new_lsp_settings)) => {
|
||||
if current_lsp_settings != new_lsp_settings {
|
||||
language_servers_to_restart.push((worktree, Arc::clone(language)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.current_lsp_settings = new_lsp_settings;
|
||||
|
||||
// Stop all newly-disabled language servers.
|
||||
for (worktree_id, adapter_name) in language_servers_to_stop {
|
||||
|
@ -134,7 +134,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||
let combined_string = location
|
||||
.paths()
|
||||
.iter()
|
||||
.map(|path| path.to_string_lossy().to_owned())
|
||||
.map(|path| util::paths::compact(&path).to_string_lossy().into_owned())
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
StringMatchCandidate::new(id, combined_string)
|
||||
|
@ -675,6 +675,9 @@ impl ProjectSearchView {
|
||||
if match_ranges.is_empty() {
|
||||
self.active_match_index = None;
|
||||
} else {
|
||||
self.active_match_index = Some(0);
|
||||
self.select_match(Direction::Next, cx);
|
||||
self.update_match_index(cx);
|
||||
let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
|
||||
let is_new_search = self.search_id != prev_search_id;
|
||||
self.results_editor.update(cx, |editor, cx| {
|
||||
|
@ -221,6 +221,14 @@ impl TerminalPanel {
|
||||
pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
|
||||
pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
|
||||
pane::Event::Focus => cx.emit(Event::Focus),
|
||||
|
||||
pane::Event::AddItem { item } => {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let pane = self.pane.clone();
|
||||
workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx))
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ impl TerminalView {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
Timer::after(CURSOR_BLINK_INTERVAL).await;
|
||||
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
|
||||
.log_err();
|
||||
.ok();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@ -907,6 +907,7 @@ mod tests {
|
||||
let params = cx.update(AppState::test);
|
||||
cx.update(|cx| {
|
||||
theme::init((), cx);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
});
|
||||
|
||||
|
@ -1030,6 +1030,7 @@ pub struct AssistantStyle {
|
||||
pub system_sender: Interactive<ContainedText>,
|
||||
pub model: Interactive<ContainedText>,
|
||||
pub remaining_tokens: ContainedText,
|
||||
pub low_remaining_tokens: ContainedText,
|
||||
pub no_remaining_tokens: ContainedText,
|
||||
pub error_icon: Icon,
|
||||
pub api_key_editor: FieldEditor,
|
||||
|
16
crates/vcs_menu/Cargo.toml
Normal file
16
crates/vcs_menu/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "vcs_menu"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
fuzzy = {path = "../fuzzy"}
|
||||
gpui = {path = "../gpui"}
|
||||
picker = {path = "../picker"}
|
||||
util = {path = "../util"}
|
||||
theme = {path = "../theme"}
|
||||
workspace = {path = "../workspace"}
|
||||
|
||||
anyhow.workspace = true
|
@ -1,17 +1,20 @@
|
||||
use anyhow::{anyhow, bail};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
elements::*, platform::MouseButton, AppContext, MouseState, Task, ViewContext, ViewHandle,
|
||||
actions, elements::*, platform::MouseButton, AppContext, MouseState, Task, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::{ops::Not, sync::Arc};
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace};
|
||||
|
||||
actions!(branches, [OpenRecent]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<BranchListDelegate>::init(cx);
|
||||
cx.add_async_action(toggle);
|
||||
}
|
||||
|
||||
pub type BranchList = Picker<BranchListDelegate>;
|
||||
|
||||
pub fn build_branch_list(
|
||||
@ -30,6 +33,34 @@ pub fn build_branch_list(
|
||||
.with_theme(|theme| theme.picker.clone())
|
||||
}
|
||||
|
||||
fn toggle(
|
||||
_: &mut Workspace,
|
||||
_: &OpenRecent,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
Some(cx.spawn(|workspace, mut cx| async move {
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
let workspace = cx.handle();
|
||||
cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
BranchListDelegate {
|
||||
matches: vec![],
|
||||
workspace,
|
||||
selected_index: 0,
|
||||
last_query: String::default(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.picker.clone())
|
||||
.with_max_size(800., 1200.)
|
||||
})
|
||||
});
|
||||
})?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
pub struct BranchListDelegate {
|
||||
matches: Vec<StringMatch>,
|
||||
workspace: ViewHandle<Workspace>,
|
@ -36,7 +36,6 @@ workspace = { path = "../workspace" }
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
parking_lot.workspace = true
|
||||
lazy_static.workspace = true
|
||||
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
|
@ -31,6 +31,8 @@ pub enum Motion {
|
||||
CurrentLine,
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
StartOfParagraph,
|
||||
EndOfParagraph,
|
||||
StartOfDocument,
|
||||
EndOfDocument,
|
||||
Matching,
|
||||
@ -72,6 +74,8 @@ actions!(
|
||||
StartOfLine,
|
||||
EndOfLine,
|
||||
CurrentLine,
|
||||
StartOfParagraph,
|
||||
EndOfParagraph,
|
||||
StartOfDocument,
|
||||
EndOfDocument,
|
||||
Matching,
|
||||
@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
|
||||
cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
|
||||
motion(Motion::StartOfParagraph, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
|
||||
motion(Motion::EndOfParagraph, cx)
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
|
||||
motion(Motion::StartOfDocument, cx)
|
||||
});
|
||||
@ -142,7 +152,8 @@ impl Motion {
|
||||
pub fn linewise(&self) -> bool {
|
||||
use Motion::*;
|
||||
match self {
|
||||
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
|
||||
Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
|
||||
| StartOfParagraph | EndOfParagraph => true,
|
||||
EndOfLine
|
||||
| NextWordEnd { .. }
|
||||
| Matching
|
||||
@ -172,6 +183,8 @@ impl Motion {
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace
|
||||
@ -197,6 +210,8 @@ impl Motion {
|
||||
| Backspace
|
||||
| Right
|
||||
| StartOfLine
|
||||
| StartOfParagraph
|
||||
| EndOfParagraph
|
||||
| NextWordStart { .. }
|
||||
| PreviousWordStart { .. }
|
||||
| FirstNonWhitespace
|
||||
@ -235,6 +250,14 @@ impl Motion {
|
||||
FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
|
||||
StartOfLine => (start_of_line(map, point), SelectionGoal::None),
|
||||
EndOfLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
StartOfParagraph => (
|
||||
movement::start_of_paragraph(map, point, times),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
EndOfParagraph => (
|
||||
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
|
||||
SelectionGoal::None,
|
||||
),
|
||||
CurrentLine => (end_of_line(map, point), SelectionGoal::None),
|
||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||
EndOfDocument => (
|
||||
@ -502,10 +525,13 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint
|
||||
if line_end == point {
|
||||
line_end = map.max_point().to_point(map);
|
||||
}
|
||||
line_end.column = line_end.column.saturating_sub(1);
|
||||
|
||||
let line_range = map.prev_line_boundary(point).0..line_end;
|
||||
let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone());
|
||||
let visible_line_range =
|
||||
line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
|
||||
let ranges = map
|
||||
.buffer_snapshot
|
||||
.bracket_ranges(visible_line_range.clone());
|
||||
if let Some(ranges) = ranges {
|
||||
let line_range = line_range.start.to_offset(&map.buffer_snapshot)
|
||||
..line_range.end.to_offset(&map.buffer_snapshot);
|
||||
@ -590,3 +616,131 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
|
||||
let new_row = (point.row() + times as u32).min(map.max_buffer_row());
|
||||
map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
mod test {
|
||||
|
||||
use crate::test::NeovimBackedTestContext;
|
||||
use indoc::indoc;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
let initial_state = indoc! {r"ˇabc
|
||||
def
|
||||
|
||||
paragraph
|
||||
the second
|
||||
|
||||
|
||||
|
||||
third and
|
||||
final"};
|
||||
|
||||
// goes down once
|
||||
cx.set_shared_state(initial_state).await;
|
||||
cx.simulate_shared_keystrokes(["}"]).await;
|
||||
cx.assert_shared_state(indoc! {r"abc
|
||||
def
|
||||
ˇ
|
||||
paragraph
|
||||
the second
|
||||
|
||||
|
||||
|
||||
third and
|
||||
final"})
|
||||
.await;
|
||||
|
||||
// goes up once
|
||||
cx.simulate_shared_keystrokes(["{"]).await;
|
||||
cx.assert_shared_state(initial_state).await;
|
||||
|
||||
// goes down twice
|
||||
cx.simulate_shared_keystrokes(["2", "}"]).await;
|
||||
cx.assert_shared_state(indoc! {r"abc
|
||||
def
|
||||
|
||||
paragraph
|
||||
the second
|
||||
ˇ
|
||||
|
||||
|
||||
third and
|
||||
final"})
|
||||
.await;
|
||||
|
||||
// goes down over multiple blanks
|
||||
cx.simulate_shared_keystrokes(["}"]).await;
|
||||
cx.assert_shared_state(indoc! {r"abc
|
||||
def
|
||||
|
||||
paragraph
|
||||
the second
|
||||
|
||||
|
||||
|
||||
third and
|
||||
finaˇl"})
|
||||
.await;
|
||||
|
||||
// goes up twice
|
||||
cx.simulate_shared_keystrokes(["2", "{"]).await;
|
||||
cx.assert_shared_state(indoc! {r"abc
|
||||
def
|
||||
ˇ
|
||||
paragraph
|
||||
the second
|
||||
|
||||
|
||||
|
||||
third and
|
||||
final"})
|
||||
.await
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_matching(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {r"func ˇ(a string) {
|
||||
do(something(with<Types>.and_arrays[0, 2]))
|
||||
}"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["%"]).await;
|
||||
cx.assert_shared_state(indoc! {r"func (a stringˇ) {
|
||||
do(something(with<Types>.and_arrays[0, 2]))
|
||||
}"})
|
||||
.await;
|
||||
|
||||
// test it works on the last character of the line
|
||||
cx.set_shared_state(indoc! {r"func (a string) ˇ{
|
||||
do(something(with<Types>.and_arrays[0, 2]))
|
||||
}"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["%"]).await;
|
||||
cx.assert_shared_state(indoc! {r"func (a string) {
|
||||
do(something(with<Types>.and_arrays[0, 2]))
|
||||
ˇ}"})
|
||||
.await;
|
||||
|
||||
// test it works on immediate nesting
|
||||
cx.set_shared_state("ˇ{()}").await;
|
||||
cx.simulate_shared_keystrokes(["%"]).await;
|
||||
cx.assert_shared_state("{()ˇ}").await;
|
||||
cx.simulate_shared_keystrokes(["%"]).await;
|
||||
cx.assert_shared_state("ˇ{()}").await;
|
||||
|
||||
// test it works on immediate nesting inside braces
|
||||
cx.set_shared_state("{\n ˇ{()}\n}").await;
|
||||
cx.simulate_shared_keystrokes(["%"]).await;
|
||||
cx.assert_shared_state("{\n {()ˇ}\n}").await;
|
||||
|
||||
// test it jumps to the next paren on a line
|
||||
cx.set_shared_state("func ˇboop() {\n}").await;
|
||||
cx.simulate_shared_keystrokes(["%"]).await;
|
||||
cx.assert_shared_state("func boop(ˇ) {\n}").await;
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,51 @@
|
||||
use editor::scroll::autoscroll::Autoscroll;
|
||||
use gpui::ViewContext;
|
||||
use language::Point;
|
||||
use language::{Bias, Point};
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::{motion::Motion, normal::ChangeCase, Vim};
|
||||
use crate::{normal::ChangeCase, state::Mode, Vim};
|
||||
|
||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
let count = vim.pop_number_operator(cx);
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.move_with(|map, selection| {
|
||||
if selection.start == selection.end {
|
||||
Motion::Right.expand_selection(map, selection, count, true);
|
||||
let mut ranges = Vec::new();
|
||||
let mut cursor_positions = Vec::new();
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
for selection in editor.selections.all::<Point>(cx) {
|
||||
match vim.state.mode {
|
||||
Mode::Visual { line: true } => {
|
||||
let start = Point::new(selection.start.row, 0);
|
||||
let end =
|
||||
Point::new(selection.end.row, snapshot.line_len(selection.end.row));
|
||||
ranges.push(start..end);
|
||||
cursor_positions.push(start..start);
|
||||
}
|
||||
Mode::Visual { line: false } => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
cursor_positions.push(selection.start..selection.start);
|
||||
}
|
||||
Mode::Insert | Mode::Normal => {
|
||||
let start = selection.start;
|
||||
let mut end = start;
|
||||
for _ in 0..count {
|
||||
end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
|
||||
}
|
||||
})
|
||||
});
|
||||
let selections = editor.selections.all::<Point>(cx);
|
||||
for selection in selections.into_iter().rev() {
|
||||
ranges.push(start..end);
|
||||
|
||||
if end.column == snapshot.line_len(end.row) {
|
||||
end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
|
||||
}
|
||||
cursor_positions.push(end..end)
|
||||
}
|
||||
}
|
||||
}
|
||||
editor.transact(cx, |editor, cx| {
|
||||
for range in ranges.into_iter().rev() {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
let range = selection.start..selection.end;
|
||||
let text = snapshot
|
||||
.text_for_range(selection.start..selection.end)
|
||||
.text_for_range(range.start..range.end)
|
||||
.flat_map(|s| s.chars())
|
||||
.flat_map(|c| {
|
||||
if c.is_lowercase() {
|
||||
@ -37,28 +59,46 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
|
||||
buffer.edit([(range, text)], None, cx)
|
||||
})
|
||||
}
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.select_ranges(cursor_positions)
|
||||
})
|
||||
});
|
||||
editor.set_clip_at_line_ends(true, cx);
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, true, cx)
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{state::Mode, test::VimTestContext};
|
||||
use indoc::indoc;
|
||||
use crate::{state::Mode, test::NeovimBackedTestContext};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_change_case(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["~"]);
|
||||
cx.assert_editor_state("AˇbC\n");
|
||||
cx.simulate_keystrokes(["2", "~"]);
|
||||
cx.assert_editor_state("ABcˇ\n");
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state("ˇabC\n").await;
|
||||
cx.simulate_shared_keystrokes(["~"]).await;
|
||||
cx.assert_shared_state("AˇbC\n").await;
|
||||
cx.simulate_shared_keystrokes(["2", "~"]).await;
|
||||
cx.assert_shared_state("ABˇc\n").await;
|
||||
|
||||
cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal);
|
||||
cx.simulate_keystrokes(["~"]);
|
||||
cx.assert_editor_state("a😀CDé1*Fˇ\n");
|
||||
// works in visual mode
|
||||
cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
|
||||
cx.simulate_shared_keystrokes(["~"]).await;
|
||||
cx.assert_shared_state("a😀CˇDé1*F\n").await;
|
||||
|
||||
// works with multibyte characters
|
||||
cx.simulate_shared_keystrokes(["~"]).await;
|
||||
cx.set_shared_state("aˇC😀é1*F\n").await;
|
||||
cx.simulate_shared_keystrokes(["4", "~"]).await;
|
||||
cx.assert_shared_state("ac😀É1ˇ*F\n").await;
|
||||
|
||||
// works with line selections
|
||||
cx.set_shared_state("abˇC\n").await;
|
||||
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
|
||||
cx.assert_shared_state("ˇABc\n").await;
|
||||
|
||||
// works with multiple cursors (zed only)
|
||||
cx.set_state("aˇßcdˇe\n", Mode::Normal);
|
||||
cx.simulate_keystroke("~");
|
||||
cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ mod neovim_connection;
|
||||
mod vim_binding_test_context;
|
||||
mod vim_test_context;
|
||||
|
||||
use command_palette::CommandPalette;
|
||||
pub use neovim_backed_binding_test_context::*;
|
||||
pub use neovim_backed_test_context::*;
|
||||
pub use vim_binding_test_context::*;
|
||||
@ -139,3 +140,16 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||
cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
|
||||
cx.assert_editor_state("aa\n b«b\n cˇ»c");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("aˇbc\n", Mode::Normal);
|
||||
cx.simulate_keystrokes(["i", "cmd-shift-p"]);
|
||||
|
||||
assert!(cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
|
||||
cx.simulate_keystroke("escape");
|
||||
assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
|
||||
cx.assert_state("aˇbc\n", Mode::Insert);
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use indoc::indoc;
|
||||
use std::ops::{Deref, DerefMut, Range};
|
||||
|
||||
use collections::{HashMap, HashSet};
|
||||
use gpui::ContextHandle;
|
||||
use language::OffsetRangeExt;
|
||||
use util::test::marked_text_offsets;
|
||||
use util::test::{generate_marked_text, marked_text_offsets};
|
||||
|
||||
use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext};
|
||||
use crate::state::Mode;
|
||||
@ -112,6 +113,43 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
context_handle
|
||||
}
|
||||
|
||||
pub async fn assert_shared_state(&mut self, marked_text: &str) {
|
||||
let neovim = self.neovim_state().await;
|
||||
if neovim != marked_text {
|
||||
panic!(
|
||||
indoc! {"Test is incorrect (currently expected != neovim state)
|
||||
|
||||
# currently expected:
|
||||
{}
|
||||
# neovim state:
|
||||
{}
|
||||
# zed state:
|
||||
{}"},
|
||||
marked_text,
|
||||
neovim,
|
||||
self.editor_state(),
|
||||
)
|
||||
}
|
||||
self.assert_editor_state(marked_text)
|
||||
}
|
||||
|
||||
pub async fn neovim_state(&mut self) -> String {
|
||||
generate_marked_text(
|
||||
self.neovim.text().await.as_str(),
|
||||
&vec![self.neovim_selection().await],
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
async fn neovim_selection(&mut self) -> Range<usize> {
|
||||
let mut neovim_selection = self.neovim.selection().await;
|
||||
// Zed selections adjust themselves to make the end point visually make sense
|
||||
if neovim_selection.start > neovim_selection.end {
|
||||
neovim_selection.start.column += 1;
|
||||
}
|
||||
neovim_selection.to_offset(&self.buffer_snapshot())
|
||||
}
|
||||
|
||||
pub async fn assert_state_matches(&mut self) {
|
||||
assert_eq!(
|
||||
self.neovim.text().await,
|
||||
@ -120,13 +158,8 @@ impl<'a> NeovimBackedTestContext<'a> {
|
||||
self.assertion_context()
|
||||
);
|
||||
|
||||
let mut neovim_selection = self.neovim.selection().await;
|
||||
// Zed selections adjust themselves to make the end point visually make sense
|
||||
if neovim_selection.start > neovim_selection.end {
|
||||
neovim_selection.start.column += 1;
|
||||
}
|
||||
let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot());
|
||||
self.assert_editor_selections(vec![neovim_selection]);
|
||||
let selections = vec![self.neovim_selection().await];
|
||||
self.assert_editor_selections(selections);
|
||||
|
||||
if let Some(neovim_mode) = self.neovim.mode().await {
|
||||
assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),);
|
||||
|
@ -11,8 +11,6 @@ use gpui::keymap_matcher::Keystroke;
|
||||
|
||||
use language::Point;
|
||||
|
||||
#[cfg(feature = "neovim")]
|
||||
use lazy_static::lazy_static;
|
||||
#[cfg(feature = "neovim")]
|
||||
use nvim_rs::{
|
||||
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
|
||||
@ -32,9 +30,7 @@ use collections::VecDeque;
|
||||
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
|
||||
// to ensure we are only constructing one neovim connection at a time.
|
||||
#[cfg(feature = "neovim")]
|
||||
lazy_static! {
|
||||
static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||
}
|
||||
static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum NeovimData {
|
||||
@ -171,15 +167,25 @@ impl NeovimConnection {
|
||||
.await
|
||||
.expect("Could not get neovim window");
|
||||
|
||||
if !selection.is_empty() {
|
||||
panic!("Setting neovim state with non empty selection not yet supported");
|
||||
}
|
||||
let cursor = selection.start;
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
|
||||
if !selection.is_empty() {
|
||||
self.nvim
|
||||
.input("v")
|
||||
.await
|
||||
.expect("could not enter visual mode");
|
||||
|
||||
let cursor = selection.end;
|
||||
nvim_window
|
||||
.set_cursor((cursor.row as i64 + 1, cursor.column as i64))
|
||||
.await
|
||||
.expect("Could not set nvim cursor position");
|
||||
}
|
||||
|
||||
if let Some(NeovimData::Get { mode, state }) = self.data.back() {
|
||||
if *mode == Some(Mode::Normal) && *state == marked_text {
|
||||
return;
|
||||
|
@ -21,12 +21,14 @@ impl<'a> VimTestContext<'a> {
|
||||
cx.update(|cx| {
|
||||
search::init(cx);
|
||||
crate::init(cx);
|
||||
command_palette::init(cx);
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<VimModeSetting>(cx, |s| *s = Some(enabled));
|
||||
});
|
||||
settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap();
|
||||
settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap();
|
||||
});
|
||||
|
||||
|
@ -12,7 +12,7 @@ mod visual;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::CommandPaletteFilter;
|
||||
use editor::{Bias, Cancel, Editor, EditorMode, Event};
|
||||
use editor::{Bias, Editor, EditorMode, Event};
|
||||
use gpui::{
|
||||
actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,
|
||||
WindowContext,
|
||||
@ -64,22 +64,6 @@ pub fn init(cx: &mut AppContext) {
|
||||
Vim::update(cx, |vim, cx| vim.push_number(n, cx));
|
||||
});
|
||||
|
||||
// Editor Actions
|
||||
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
||||
// If we are in aren't in normal mode or have an active operator, swap to normal mode
|
||||
// Otherwise forward cancel on to the editor
|
||||
let vim = Vim::read(cx);
|
||||
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
|
||||
WindowContext::defer(cx, |cx| {
|
||||
Vim::update(cx, |state, cx| {
|
||||
state.switch_mode(Mode::Normal, false, cx);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
cx.propagate_action();
|
||||
}
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Tab, cx| {
|
||||
Vim::active_editor_input_ignored(" ".into(), cx)
|
||||
});
|
||||
@ -109,10 +93,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
|
||||
if let Some(handled_by) = handled_by {
|
||||
// Keystroke is handled by the vim system, so continue forward
|
||||
// Also short circuit if it is the special cancel action
|
||||
if handled_by.namespace() == "vim"
|
||||
|| (handled_by.namespace() == "editor" && handled_by.name() == "Cancel")
|
||||
{
|
||||
if handled_by.namespace() == "vim" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
18
crates/vim/test_data/test_change_case.json
Normal file
18
crates/vim/test_data/test_change_case.json
Normal file
@ -0,0 +1,18 @@
|
||||
{"Put":{"state":"ˇabC\n"}}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"AˇbC\n","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ABˇc\n","mode":"Normal"}}
|
||||
{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}}
|
||||
{"Key":"~"}
|
||||
{"Put":{"state":"aˇC😀é1*F\n"}}
|
||||
{"Key":"4"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}}
|
||||
{"Put":{"state":"abˇC\n"}}
|
||||
{"Key":"shift-v"}
|
||||
{"Key":"~"}
|
||||
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
|
17
crates/vim/test_data/test_matching.json
Normal file
17
crates/vim/test_data/test_matching.json
Normal file
@ -0,0 +1,17 @@
|
||||
{"Put":{"state":"func ˇ(a string) {\n do(something(with<Types>.and_arrays[0, 2]))\n}"}}
|
||||
{"Key":"%"}
|
||||
{"Get":{"state":"func (a stringˇ) {\n do(something(with<Types>.and_arrays[0, 2]))\n}","mode":"Normal"}}
|
||||
{"Put":{"state":"func (a string) ˇ{\ndo(something(with<Types>.and_arrays[0, 2]))\n}"}}
|
||||
{"Key":"%"}
|
||||
{"Get":{"state":"func (a string) {\ndo(something(with<Types>.and_arrays[0, 2]))\nˇ}","mode":"Normal"}}
|
||||
{"Put":{"state":"ˇ{()}"}}
|
||||
{"Key":"%"}
|
||||
{"Get":{"state":"{()ˇ}","mode":"Normal"}}
|
||||
{"Key":"%"}
|
||||
{"Get":{"state":"ˇ{()}","mode":"Normal"}}
|
||||
{"Put":{"state":"{\n ˇ{()}\n}"}}
|
||||
{"Key":"%"}
|
||||
{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}}
|
||||
{"Put":{"state":"func ˇboop() {\n}"}}
|
||||
{"Key":"%"}
|
||||
{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}}
|
13
crates/vim/test_data/test_start_end_of_paragraph.json
Normal file
13
crates/vim/test_data/test_start_end_of_paragraph.json
Normal file
@ -0,0 +1,13 @@
|
||||
{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}}
|
||||
{"Key":"}"}
|
||||
{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
|
||||
{"Key":"{"}
|
||||
{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"}"}
|
||||
{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}}
|
||||
{"Key":"}"}
|
||||
{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}}
|
||||
{"Key":"2"}
|
||||
{"Key":"{"}
|
||||
{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}}
|
@ -27,7 +27,7 @@ use std::{
|
||||
};
|
||||
use theme::Theme;
|
||||
|
||||
#[derive(Eq, PartialEq, Hash)]
|
||||
#[derive(Eq, PartialEq, Hash, Debug)]
|
||||
pub enum ItemEvent {
|
||||
CloseItem,
|
||||
UpdateTab,
|
||||
|
@ -2316,6 +2316,7 @@ mod tests {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
crate::init_settings(cx);
|
||||
Project::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -57,8 +57,9 @@ use staff_mode::StaffMode;
|
||||
use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
|
||||
use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace};
|
||||
use zed::{
|
||||
assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace,
|
||||
languages, menus,
|
||||
assets::Assets,
|
||||
build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus,
|
||||
only_instance::{ensure_only_instance, IsOnlyInstance},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
@ -66,6 +67,10 @@ fn main() {
|
||||
init_paths();
|
||||
init_logger();
|
||||
|
||||
if ensure_only_instance() != IsOnlyInstance::Yes {
|
||||
return;
|
||||
}
|
||||
|
||||
log::info!("========== starting zed ==========");
|
||||
let mut app = gpui::App::new(Assets).unwrap();
|
||||
|
||||
|
103
crates/zed/src/only_instance.rs
Normal file
103
crates/zed/src/only_instance.rs
Normal file
@ -0,0 +1,103 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use util::channel::ReleaseChannel;
|
||||
|
||||
const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1);
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_millis(10);
|
||||
const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35);
|
||||
const SEND_TIMEOUT: Duration = Duration::from_millis(20);
|
||||
|
||||
fn address() -> SocketAddr {
|
||||
let port = match *util::channel::RELEASE_CHANNEL {
|
||||
ReleaseChannel::Dev => 43737,
|
||||
ReleaseChannel::Preview => 43738,
|
||||
ReleaseChannel::Stable => 43739,
|
||||
};
|
||||
|
||||
SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port))
|
||||
}
|
||||
|
||||
fn instance_handshake() -> &'static str {
|
||||
match *util::channel::RELEASE_CHANNEL {
|
||||
ReleaseChannel::Dev => "Zed Editor Dev Instance Running",
|
||||
ReleaseChannel::Preview => "Zed Editor Preview Instance Running",
|
||||
ReleaseChannel::Stable => "Zed Editor Stable Instance Running",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum IsOnlyInstance {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
pub fn ensure_only_instance() -> IsOnlyInstance {
|
||||
if *db::ZED_STATELESS {
|
||||
return IsOnlyInstance::Yes;
|
||||
}
|
||||
|
||||
if check_got_handshake() {
|
||||
return IsOnlyInstance::No;
|
||||
}
|
||||
|
||||
let listener = match TcpListener::bind(address()) {
|
||||
Ok(listener) => listener,
|
||||
|
||||
Err(err) => {
|
||||
log::warn!("Error binding to single instance port: {err}");
|
||||
if check_got_handshake() {
|
||||
return IsOnlyInstance::No;
|
||||
}
|
||||
|
||||
// Avoid failing to start when some other application by chance already has
|
||||
// a claim on the port. This is sub-par as any other instance that gets launched
|
||||
// will be unable to communicate with this instance and will duplicate
|
||||
log::warn!("Backup handshake request failed, continuing without handshake");
|
||||
return IsOnlyInstance::Yes;
|
||||
}
|
||||
};
|
||||
|
||||
thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
let mut stream = match stream {
|
||||
Ok(stream) => stream,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
_ = stream.set_nodelay(true);
|
||||
_ = stream.set_read_timeout(Some(SEND_TIMEOUT));
|
||||
_ = stream.write_all(instance_handshake().as_bytes());
|
||||
}
|
||||
});
|
||||
|
||||
IsOnlyInstance::Yes
|
||||
}
|
||||
|
||||
fn check_got_handshake() -> bool {
|
||||
match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) {
|
||||
Ok(mut stream) => {
|
||||
let mut buf = vec![0u8; instance_handshake().len()];
|
||||
|
||||
stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap();
|
||||
if let Err(err) = stream.read_exact(&mut buf) {
|
||||
log::warn!("Connected to single instance port but failed to read: {err}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if buf == instance_handshake().as_bytes() {
|
||||
log::info!("Got instance handshake");
|
||||
return true;
|
||||
}
|
||||
|
||||
log::warn!("Got wrong instance handshake value");
|
||||
false
|
||||
}
|
||||
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
pub mod assets;
|
||||
pub mod languages;
|
||||
pub mod menus;
|
||||
pub mod only_instance;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
|
55
styles/src/component/tab_bar_button.ts
Normal file
55
styles/src/component/tab_bar_button.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Theme, StyleSets } from "../common"
|
||||
import { interactive } from "../element"
|
||||
import { InteractiveState } from "../element/interactive"
|
||||
import { background, foreground } from "../style_tree/components"
|
||||
|
||||
interface TabBarButtonOptions {
|
||||
icon: string
|
||||
color?: StyleSets
|
||||
}
|
||||
|
||||
type TabBarButtonProps = TabBarButtonOptions & {
|
||||
state?: Partial<Record<InteractiveState, Partial<TabBarButtonOptions>>>
|
||||
}
|
||||
|
||||
export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) {
|
||||
const button_spacing = 8
|
||||
|
||||
return (
|
||||
interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.middle, color),
|
||||
asset: icon,
|
||||
dimensions: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
corner_radius: 4,
|
||||
padding: {
|
||||
top: 4, bottom: 4, left: 4, right: 4
|
||||
},
|
||||
margin: {
|
||||
left: button_spacing / 2,
|
||||
right: button_spacing / 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
container: {
|
||||
background: background(theme.middle, color, "hovered"),
|
||||
|
||||
}
|
||||
},
|
||||
clicked: {
|
||||
container: {
|
||||
background: background(theme.middle, color, "pressed"),
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
@ -1,233 +1,133 @@
|
||||
import { text, border, background, foreground } from "./components"
|
||||
import { interactive } from "../element"
|
||||
import { useTheme } from "../theme"
|
||||
import { text, border, background, foreground, TextStyle } from "./components"
|
||||
import { Interactive, interactive } from "../element"
|
||||
import { tab_bar_button } from "../component/tab_bar_button"
|
||||
import { StyleSets, useTheme } from "../theme"
|
||||
|
||||
type RoleCycleButton = TextStyle & {
|
||||
background?: string
|
||||
}
|
||||
// TODO: Replace these with zed types
|
||||
type RemainingTokens = TextStyle & {
|
||||
background: string,
|
||||
margin: { top: number, right: number },
|
||||
padding: {
|
||||
right: number,
|
||||
left: number,
|
||||
top: number,
|
||||
bottom: number,
|
||||
},
|
||||
corner_radius: number,
|
||||
}
|
||||
|
||||
export default function assistant(): any {
|
||||
const theme = useTheme()
|
||||
|
||||
const interactive_role = (color: StyleSets): Interactive<RoleCycleButton> => {
|
||||
return (
|
||||
interactive({
|
||||
base: {
|
||||
...text(theme.highest, "sans", color, { size: "sm" }),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(theme.highest, "sans", color, { size: "sm" }),
|
||||
background: background(theme.highest, color, "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
...text(theme.highest, "sans", color, { size: "sm" }),
|
||||
background: background(theme.highest, color, "pressed"),
|
||||
}
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const tokens_remaining = (color: StyleSets): RemainingTokens => {
|
||||
return (
|
||||
{
|
||||
...text(theme.highest, "mono", color, { size: "xs" }),
|
||||
background: background(theme.highest, "on", "default"),
|
||||
margin: { top: 12, right: 20 },
|
||||
padding: { right: 4, left: 4, top: 1, bottom: 1 },
|
||||
corner_radius: 6,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
container: {
|
||||
background: background(theme.highest),
|
||||
padding: { left: 12 },
|
||||
},
|
||||
message_header: {
|
||||
margin: { bottom: 6, top: 6 },
|
||||
margin: { bottom: 4, top: 4 },
|
||||
background: background(theme.highest),
|
||||
},
|
||||
hamburger_button: interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "variant"),
|
||||
asset: "icons/hamburger_15.svg",
|
||||
dimensions: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: { left: 12, right: 8.5 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
hamburger_button: tab_bar_button(theme, {
|
||||
icon: "icons/hamburger_15.svg",
|
||||
}),
|
||||
split_button: interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "variant"),
|
||||
asset: "icons/split_message_15.svg",
|
||||
dimensions: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: { left: 8.5, right: 8.5 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
split_button: tab_bar_button(theme, {
|
||||
icon: "icons/split_message_15.svg",
|
||||
}),
|
||||
quote_button: interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "variant"),
|
||||
asset: "icons/quote_15.svg",
|
||||
dimensions: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: { left: 8.5, right: 8.5 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
quote_button: tab_bar_button(theme, {
|
||||
icon: "icons/radix/quote.svg",
|
||||
}),
|
||||
assist_button: interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "variant"),
|
||||
asset: "icons/assist_15.svg",
|
||||
dimensions: {
|
||||
width: 15,
|
||||
height: 15,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: { left: 8.5, right: 8.5 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
assist_button: tab_bar_button(theme, {
|
||||
icon: "icons/radix/magic-wand.svg",
|
||||
}),
|
||||
zoom_in_button: interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "variant"),
|
||||
asset: "icons/maximize_8.svg",
|
||||
dimensions: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: { left: 10, right: 10 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
zoom_in_button: tab_bar_button(theme, {
|
||||
icon: "icons/radix/maximize.svg",
|
||||
}),
|
||||
zoom_out_button: interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "variant"),
|
||||
asset: "icons/minimize_8.svg",
|
||||
dimensions: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: { left: 10, right: 10 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
zoom_out_button: tab_bar_button(theme, {
|
||||
icon: "icons/radix/minimize.svg",
|
||||
}),
|
||||
plus_button: interactive({
|
||||
base: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "variant"),
|
||||
asset: "icons/plus_12.svg",
|
||||
dimensions: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
},
|
||||
},
|
||||
container: {
|
||||
padding: { left: 10, right: 10 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
icon: {
|
||||
color: foreground(theme.highest, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
plus_button: tab_bar_button(theme, {
|
||||
icon: "icons/radix/plus.svg",
|
||||
}),
|
||||
title: {
|
||||
...text(theme.highest, "sans", "default", { size: "sm" }),
|
||||
...text(theme.highest, "sans", "default", { size: "xs" }),
|
||||
},
|
||||
saved_conversation: {
|
||||
container: interactive({
|
||||
base: {
|
||||
background: background(theme.highest, "on"),
|
||||
background: background(theme.middle),
|
||||
padding: { top: 4, bottom: 4 },
|
||||
border: border(theme.middle, "default", { top: true, overlay: true }),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(theme.highest, "on", "hovered"),
|
||||
background: background(theme.middle, "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
background: background(theme.middle, "pressed"),
|
||||
}
|
||||
},
|
||||
}),
|
||||
saved_at: {
|
||||
margin: { left: 8 },
|
||||
...text(theme.highest, "sans", "default", { size: "xs" }),
|
||||
...text(theme.highest, "sans", "variant", { size: "xs" }),
|
||||
},
|
||||
title: {
|
||||
margin: { left: 16 },
|
||||
margin: { left: 12 },
|
||||
...text(theme.highest, "sans", "default", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
},
|
||||
user_sender: {
|
||||
default: {
|
||||
...text(theme.highest, "sans", "default", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
},
|
||||
assistant_sender: {
|
||||
default: {
|
||||
...text(theme.highest, "sans", "accent", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
},
|
||||
system_sender: {
|
||||
default: {
|
||||
...text(theme.highest, "sans", "variant", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
},
|
||||
user_sender: interactive_role("base"),
|
||||
assistant_sender: interactive_role("accent"),
|
||||
system_sender: interactive_role("warning"),
|
||||
sent_at: {
|
||||
margin: { top: 2, left: 8 },
|
||||
...text(theme.highest, "sans", "default", { size: "2xs" }),
|
||||
...text(theme.highest, "sans", "variant", { size: "2xs" }),
|
||||
},
|
||||
model: interactive({
|
||||
base: {
|
||||
background: background(theme.highest, "on"),
|
||||
margin: { left: 12, right: 12, top: 12 },
|
||||
padding: 4,
|
||||
background: background(theme.highest),
|
||||
margin: { left: 12, right: 4, top: 12 },
|
||||
padding: { right: 4, left: 4, top: 1, bottom: 1 },
|
||||
corner_radius: 4,
|
||||
...text(theme.highest, "sans", "default", { size: "xs" }),
|
||||
},
|
||||
@ -238,20 +138,9 @@ export default function assistant(): any {
|
||||
},
|
||||
},
|
||||
}),
|
||||
remaining_tokens: {
|
||||
background: background(theme.highest, "on"),
|
||||
margin: { top: 12, right: 24 },
|
||||
padding: 4,
|
||||
corner_radius: 4,
|
||||
...text(theme.highest, "sans", "positive", { size: "xs" }),
|
||||
},
|
||||
no_remaining_tokens: {
|
||||
background: background(theme.highest, "on"),
|
||||
margin: { top: 12, right: 24 },
|
||||
padding: 4,
|
||||
corner_radius: 4,
|
||||
...text(theme.highest, "sans", "negative", { size: "xs" }),
|
||||
},
|
||||
remaining_tokens: tokens_remaining("positive"),
|
||||
low_remaining_tokens: tokens_remaining("warning"),
|
||||
no_remaining_tokens: tokens_remaining("negative"),
|
||||
error_icon: {
|
||||
margin: { left: 8 },
|
||||
color: foreground(theme.highest, "negative"),
|
||||
@ -259,7 +148,7 @@ export default function assistant(): any {
|
||||
},
|
||||
api_key_editor: {
|
||||
background: background(theme.highest, "on"),
|
||||
corner_radius: 6,
|
||||
corner_radius: 4,
|
||||
text: text(theme.highest, "mono", "on"),
|
||||
placeholder_text: text(theme.highest, "mono", "on", "disabled", {
|
||||
size: "xs",
|
||||
|
@ -84,7 +84,7 @@ function user_menu() {
|
||||
base: {
|
||||
corner_radius: 6,
|
||||
height: button_height,
|
||||
width: online ? 37 : 24,
|
||||
width: 20,
|
||||
padding: {
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
@ -153,6 +153,7 @@ function user_menu() {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
user_menu_button_online: build_button({ online: true }),
|
||||
user_menu_button_offline: build_button({ online: false }),
|
||||
|
@ -12,8 +12,17 @@ export interface Theme {
|
||||
name: string
|
||||
is_light: boolean
|
||||
|
||||
/**
|
||||
* App background, other elements that should sit directly on top of the background.
|
||||
*/
|
||||
lowest: Layer
|
||||
/**
|
||||
* Panels, tabs, other UI surfaces that sit on top of the background.
|
||||
*/
|
||||
middle: Layer
|
||||
/**
|
||||
* Editors like code buffers, conversation editors, etc.
|
||||
*/
|
||||
highest: Layer
|
||||
|
||||
ramps: RampSet
|
||||
|
Loading…
Reference in New Issue
Block a user