brought up to speed with main
This commit is contained in:
@@ -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
|
||||
};
|
||||
|
||||
+41
-2
@@ -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)]
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -41,8 +41,7 @@ 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());
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -90,7 +90,8 @@ pub struct LanguageServerName(pub Arc<str>);
|
||||
/// once at startup, and caches the results.
|
||||
pub struct CachedLspAdapter {
|
||||
pub name: LanguageServerName,
|
||||
pub initialization_options: Option<Value>,
|
||||
initialization_options: Option<Value>,
|
||||
initialization_overrides: Mutex<Option<Value>>,
|
||||
pub disk_based_diagnostic_sources: Vec<String>,
|
||||
pub disk_based_diagnostics_progress_token: Option<String>,
|
||||
pub language_ids: HashMap<String, String>,
|
||||
@@ -109,6 +110,7 @@ impl CachedLspAdapter {
|
||||
Arc::new(CachedLspAdapter {
|
||||
name,
|
||||
initialization_options,
|
||||
initialization_overrides: Mutex::new(None),
|
||||
disk_based_diagnostic_sources,
|
||||
disk_based_diagnostics_progress_token,
|
||||
language_ids,
|
||||
@@ -208,6 +210,30 @@ impl CachedLspAdapter {
|
||||
) -> Option<CodeLabel> {
|
||||
self.adapter.label_for_symbol(name, kind, language).await
|
||||
}
|
||||
|
||||
pub fn update_initialization_overrides(&self, new: Option<&Value>) -> bool {
|
||||
let mut current = self.initialization_overrides.lock();
|
||||
if current.as_ref() != new {
|
||||
*current = new.cloned();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn initialization_options(&self) -> Option<Value> {
|
||||
let initialization_options = self.initialization_options.as_ref();
|
||||
let override_options = self.initialization_overrides.lock().clone();
|
||||
match (initialization_options, override_options) {
|
||||
(None, override_options) => override_options,
|
||||
(initialization_options, None) => initialization_options.cloned(),
|
||||
(Some(initialization_options), Some(override_options)) => {
|
||||
let mut initialization_options = initialization_options.clone();
|
||||
merge_json_value_into(override_options, &mut initialization_options);
|
||||
Some(initialization_options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LspAdapterDelegate: Send + Sync {
|
||||
@@ -428,6 +454,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>,
|
||||
@@ -1681,6 +1708,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(),
|
||||
}
|
||||
}
|
||||
@@ -1730,6 +1758,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>)]) {
|
||||
|
||||
@@ -78,8 +78,8 @@ use std::{
|
||||
use terminals::Terminals;
|
||||
use text::Anchor;
|
||||
use util::{
|
||||
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
||||
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
||||
debug_panic, defer, http::HttpClient, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt,
|
||||
TryFutureExt as _,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
@@ -801,7 +801,7 @@ impl Project {
|
||||
.lsp
|
||||
.get(&adapter.name.0)
|
||||
.and_then(|s| s.initialization_options.as_ref());
|
||||
if adapter.initialization_options.as_ref() != new_lsp_settings {
|
||||
if adapter.update_initialization_overrides(new_lsp_settings) {
|
||||
language_servers_to_restart.push((worktree, Arc::clone(language)));
|
||||
}
|
||||
}
|
||||
@@ -2546,20 +2546,13 @@ impl Project {
|
||||
let project_settings = settings::get::<ProjectSettings>(cx);
|
||||
let lsp = project_settings.lsp.get(&adapter.name.0);
|
||||
let override_options = lsp.map(|s| s.initialization_options.clone()).flatten();
|
||||
|
||||
let mut initialization_options = adapter.initialization_options.clone();
|
||||
match (&mut initialization_options, override_options) {
|
||||
(Some(initialization_options), Some(override_options)) => {
|
||||
merge_json_value_into(override_options, initialization_options);
|
||||
}
|
||||
(None, override_options) => initialization_options = override_options,
|
||||
_ => {}
|
||||
}
|
||||
adapter.update_initialization_overrides(override_options.as_ref());
|
||||
|
||||
let server_id = pending_server.server_id;
|
||||
let container_dir = pending_server.container_dir.clone();
|
||||
let state = LanguageServerState::Starting({
|
||||
let adapter = adapter.clone();
|
||||
let initialization_options = adapter.initialization_options();
|
||||
let server_name = adapter.name.0.clone();
|
||||
let languages = self.languages.clone();
|
||||
let language = language.clone();
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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,15 +1,17 @@
|
||||
use anyhow::{anyhow, bail};
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
|
||||
use gpui::{actions, elements::*, 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(
|
||||
@@ -28,6 +30,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>,
|
||||
+157
-3
@@ -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(),);
|
||||
|
||||
@@ -167,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();
|
||||
});
|
||||
|
||||
|
||||
+2
-21
@@ -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"}}
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user