Detailed changes
@@ -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 }}
- ```
@@ -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" }
@@ -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>
@@ -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>
@@ -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,
@@ -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;
+
+ // 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;
- 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 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;
}
}
@@ -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"}}
@@ -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"}}
@@ -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();
@@ -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;
@@ -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 },
- ...text(theme.highest, "sans", "default", {
- size: "sm",
- weight: "bold",
- }),
- },
- },
- user_sender: {
- default: {
+ margin: { left: 12 },
...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