Merge branch 'main' into randomized-tests-operation-script
This commit is contained in:
commit
5a4fa4b11e
78 changed files with 2084 additions and 1667 deletions
43
Cargo.lock
generated
43
Cargo.lock
generated
|
@ -2759,6 +2759,12 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "human_bytes"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39b528196c838e8b3da8b665e08c30958a6f2ede91d79f2ffcd0d4664b9c64eb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "humantime"
|
name = "humantime"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
@ -3757,6 +3763,15 @@ dependencies = [
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ntapi"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
|
||||||
|
dependencies = [
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nu-ansi-term"
|
name = "nu-ansi-term"
|
||||||
version = "0.46.0"
|
version = "0.46.0"
|
||||||
|
@ -4426,7 +4441,7 @@ source = "git+https://github.com/zed-industries/wezterm?rev=5cd757e5f2eb039ed0c6
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"ntapi",
|
"ntapi 0.3.7",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -6222,6 +6237,21 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sysinfo"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccb297c0afb439440834b4bcf02c5c9da8ec2e808e70f36b0d8e815ff403bd24"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.0",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
"ntapi 0.4.0",
|
||||||
|
"once_cell",
|
||||||
|
"rayon",
|
||||||
|
"winapi 0.3.9",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "system-interface"
|
name = "system-interface"
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
|
@ -7204,6 +7234,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.14.1"
|
version = "0.14.1"
|
||||||
|
@ -8151,7 +8187,7 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.68.0"
|
version = "0.69.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
@ -8183,6 +8219,7 @@ dependencies = [
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"go_to_line",
|
"go_to_line",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"human_bytes",
|
||||||
"ignore",
|
"ignore",
|
||||||
"image",
|
"image",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
@ -8216,6 +8253,7 @@ dependencies = [
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
|
"sysinfo",
|
||||||
"tempdir",
|
"tempdir",
|
||||||
"terminal_view",
|
"terminal_view",
|
||||||
"text",
|
"text",
|
||||||
|
@ -8244,6 +8282,7 @@ dependencies = [
|
||||||
"tree-sitter-typescript",
|
"tree-sitter-typescript",
|
||||||
"unindent",
|
"unindent",
|
||||||
"url",
|
"url",
|
||||||
|
"urlencoding",
|
||||||
"util",
|
"util",
|
||||||
"vim",
|
"vim",
|
||||||
"workspace",
|
"workspace",
|
||||||
|
|
|
@ -20,8 +20,10 @@
|
||||||
"alt-cmd-left": "pane::ActivatePrevItem",
|
"alt-cmd-left": "pane::ActivatePrevItem",
|
||||||
"alt-cmd-right": "pane::ActivateNextItem",
|
"alt-cmd-right": "pane::ActivateNextItem",
|
||||||
"cmd-w": "pane::CloseActiveItem",
|
"cmd-w": "pane::CloseActiveItem",
|
||||||
"cmd-shift-w": "workspace::CloseWindow",
|
|
||||||
"alt-cmd-t": "pane::CloseInactiveItems",
|
"alt-cmd-t": "pane::CloseInactiveItems",
|
||||||
|
"cmd-k u": "pane::CloseCleanItems",
|
||||||
|
"cmd-k cmd-w": "pane::CloseAllItems",
|
||||||
|
"cmd-shift-w": "workspace::CloseWindow",
|
||||||
"cmd-s": "workspace::Save",
|
"cmd-s": "workspace::Save",
|
||||||
"cmd-shift-s": "workspace::SaveAs",
|
"cmd-shift-s": "workspace::SaveAs",
|
||||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||||
|
@ -67,9 +69,11 @@
|
||||||
"up": "editor::MoveUp",
|
"up": "editor::MoveUp",
|
||||||
"pageup": "editor::PageUp",
|
"pageup": "editor::PageUp",
|
||||||
"shift-pageup": "editor::MovePageUp",
|
"shift-pageup": "editor::MovePageUp",
|
||||||
|
"home": "editor::MoveToBeginningOfLine",
|
||||||
"down": "editor::MoveDown",
|
"down": "editor::MoveDown",
|
||||||
"pagedown": "editor::PageDown",
|
"pagedown": "editor::PageDown",
|
||||||
"shift-pagedown": "editor::MovePageDown",
|
"shift-pagedown": "editor::MovePageDown",
|
||||||
|
"end": "editor::MoveToEndOfLine",
|
||||||
"left": "editor::MoveLeft",
|
"left": "editor::MoveLeft",
|
||||||
"right": "editor::MoveRight",
|
"right": "editor::MoveRight",
|
||||||
"ctrl-p": "editor::MoveUp",
|
"ctrl-p": "editor::MoveUp",
|
||||||
|
@ -110,6 +114,12 @@
|
||||||
"stop_at_soft_wraps": true
|
"stop_at_soft_wraps": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"shift-home": [
|
||||||
|
"editor::SelectToBeginningOfLine",
|
||||||
|
{
|
||||||
|
"stop_at_soft_wraps": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"ctrl-shift-a": [
|
"ctrl-shift-a": [
|
||||||
"editor::SelectToBeginningOfLine",
|
"editor::SelectToBeginningOfLine",
|
||||||
{
|
{
|
||||||
|
@ -122,6 +132,12 @@
|
||||||
"stop_at_soft_wraps": true
|
"stop_at_soft_wraps": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"shift-end": [
|
||||||
|
"editor::SelectToEndOfLine",
|
||||||
|
{
|
||||||
|
"stop_at_soft_wraps": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"ctrl-shift-e": [
|
"ctrl-shift-e": [
|
||||||
"editor::SelectToEndOfLine",
|
"editor::SelectToEndOfLine",
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"context": "Editor && VimControl",
|
"context": "Editor && VimControl && !VimWaiting",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"g": [
|
"g": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
|
@ -53,6 +53,42 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"%": "vim::Matching",
|
"%": "vim::Matching",
|
||||||
|
"ctrl-y": [
|
||||||
|
"vim::Scroll",
|
||||||
|
"LineUp"
|
||||||
|
],
|
||||||
|
"f": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"FindForward": {
|
||||||
|
"before": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"t": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"FindForward": {
|
||||||
|
"before": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shift-f": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"FindBackward": {
|
||||||
|
"after": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"shift-t": [
|
||||||
|
"vim::PushOperator",
|
||||||
|
{
|
||||||
|
"FindBackward": {
|
||||||
|
"after": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"escape": "editor::Cancel",
|
"escape": "editor::Cancel",
|
||||||
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
|
||||||
"1": [
|
"1": [
|
||||||
|
@ -94,7 +130,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == normal && vim_operator == none",
|
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"c": [
|
"c": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
|
@ -173,10 +209,6 @@
|
||||||
"ctrl-e": [
|
"ctrl-e": [
|
||||||
"vim::Scroll",
|
"vim::Scroll",
|
||||||
"LineDown"
|
"LineDown"
|
||||||
],
|
|
||||||
"ctrl-y": [
|
|
||||||
"vim::Scroll",
|
|
||||||
"LineUp"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -255,7 +287,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && vim_mode == visual",
|
"context": "Editor && vim_mode == visual && !VimWaiting",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"u": "editor::Undo",
|
"u": "editor::Undo",
|
||||||
"c": "vim::VisualChange",
|
"c": "vim::VisualChange",
|
||||||
|
@ -271,5 +303,11 @@
|
||||||
"escape": "vim::NormalBefore",
|
"escape": "vim::NormalBefore",
|
||||||
"ctrl-c": "vim::NormalBefore"
|
"ctrl-c": "vim::NormalBefore"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && VimWaiting",
|
||||||
|
"bindings": {
|
||||||
|
"*": "gpui::KeyPressed"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
|
@ -221,7 +221,7 @@
|
||||||
// rust-analyzer
|
// rust-analyzer
|
||||||
// typescript-language-server
|
// typescript-language-server
|
||||||
// vscode-json-languageserver
|
// vscode-json-languageserver
|
||||||
// "rust_analyzer": {
|
// "rust-analyzer": {
|
||||||
// //These initialization options are merged into Zed's defaults
|
// //These initialization options are merged into Zed's defaults
|
||||||
// "initialization_options": {
|
// "initialization_options": {
|
||||||
// "checkOnSave": {
|
// "checkOnSave": {
|
||||||
|
|
|
@ -1131,6 +1131,7 @@ async fn test_unshare_project(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
let worktree_a = project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
|
||||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||||
|
|
||||||
project_b
|
project_b
|
||||||
|
@ -1160,6 +1161,7 @@ async fn test_unshare_project(
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
|
let project_c2 = client_c.build_remote_project(project_id, cx_c).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||||
project_c2
|
project_c2
|
||||||
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
.update(cx_c, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
|
||||||
|
@ -1213,6 +1215,7 @@ async fn test_host_disconnect(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
|
||||||
|
|
||||||
let (_, workspace_b) = cx_b.add_window(|cx| {
|
let (_, workspace_b) = cx_b.add_window(|cx| {
|
||||||
|
@ -1467,7 +1470,7 @@ async fn test_project_reconnect(
|
||||||
.read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
|
.read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||||
.await;
|
.await;
|
||||||
let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
|
let worktree3_id = worktree_a3.read_with(cx_a, |tree, _| {
|
||||||
assert!(tree.as_local().unwrap().is_shared());
|
assert!(!tree.as_local().unwrap().is_shared());
|
||||||
tree.id()
|
tree.id()
|
||||||
});
|
});
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
|
@ -1489,6 +1492,7 @@ async fn test_project_reconnect(
|
||||||
deterministic.run_until_parked();
|
deterministic.run_until_parked();
|
||||||
project_a1.read_with(cx_a, |project, cx| {
|
project_a1.read_with(cx_a, |project, cx| {
|
||||||
assert!(project.is_shared());
|
assert!(project.is_shared());
|
||||||
|
assert!(worktree_a1.read(cx).as_local().unwrap().is_shared());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
worktree_a1
|
worktree_a1
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
@ -1510,6 +1514,7 @@ async fn test_project_reconnect(
|
||||||
"subdir2/i.txt"
|
"subdir2/i.txt"
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
assert!(worktree_a3.read(cx).as_local().unwrap().is_shared());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
worktree_a3
|
worktree_a3
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
|
|
@ -15,7 +15,10 @@ use language::{range_to_lsp, FakeLspAdapter, Language, LanguageConfig, PointUtf1
|
||||||
use lsp::FakeLanguageServer;
|
use lsp::FakeLanguageServer;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{search::SearchQuery, Project, ProjectPath};
|
use project::{search::SearchQuery, Project, ProjectPath};
|
||||||
use rand::prelude::*;
|
use rand::{
|
||||||
|
distributions::{Alphanumeric, DistString},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
|
@ -293,9 +296,15 @@ async fn test_random_collaboration(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
(None, None) => {}
|
(None, None) => {}
|
||||||
(None, _) => panic!("host's file is None, guest's isn't "),
|
(None, _) => panic!("host's file is None, guest's isn't"),
|
||||||
(_, None) => panic!("guest's file is None, hosts's isn't "),
|
(_, None) => panic!("guest's file is None, hosts's isn't"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let host_diff_base =
|
||||||
|
host_buffer.read_with(host_cx, |b, _| b.diff_base().map(ToString::to_string));
|
||||||
|
let guest_diff_base = guest_buffer
|
||||||
|
.read_with(client_cx, |b, _| b.diff_base().map(ToString::to_string));
|
||||||
|
assert_eq!(guest_diff_base, host_diff_base);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -918,6 +927,37 @@ async fn apply_client_operation(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClientOperation::WriteGitIndex {
|
||||||
|
repo_path,
|
||||||
|
contents,
|
||||||
|
} => {
|
||||||
|
if !client
|
||||||
|
.fs
|
||||||
|
.metadata(&repo_path)
|
||||||
|
.await?
|
||||||
|
.map_or(false, |m| m.is_dir)
|
||||||
|
{
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!(
|
||||||
|
"{}: writing git index for repo {:?}: {:?}",
|
||||||
|
client.username,
|
||||||
|
repo_path,
|
||||||
|
contents
|
||||||
|
);
|
||||||
|
|
||||||
|
let dot_git_dir = repo_path.join(".git");
|
||||||
|
let contents = contents
|
||||||
|
.iter()
|
||||||
|
.map(|(path, contents)| (path.as_path(), contents.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if client.fs.metadata(&dot_git_dir).await?.is_none() {
|
||||||
|
client.fs.create_dir(&dot_git_dir).await?;
|
||||||
|
}
|
||||||
|
client.fs.set_index_for_repo(&dot_git_dir, &contents).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
@ -1038,6 +1078,10 @@ enum ClientOperation {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
is_dir: bool,
|
is_dir: bool,
|
||||||
},
|
},
|
||||||
|
WriteGitIndex {
|
||||||
|
repo_path: PathBuf,
|
||||||
|
contents: Vec<(PathBuf, String)>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@ -1221,6 +1265,7 @@ impl TestPlan {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let executor = cx.background();
|
||||||
self.operation_ix += 1;
|
self.operation_ix += 1;
|
||||||
let call = cx.read(ActiveCall::global);
|
let call = cx.read(ActiveCall::global);
|
||||||
Some(loop {
|
Some(loop {
|
||||||
|
@ -1337,7 +1382,7 @@ impl TestPlan {
|
||||||
.choose(&mut self.rng)
|
.choose(&mut self.rng)
|
||||||
.cloned() else { continue };
|
.cloned() else { continue };
|
||||||
let project_root_name = root_name_for_project(&project, cx);
|
let project_root_name = root_name_for_project(&project, cx);
|
||||||
let mut paths = cx.background().block(client.fs.paths());
|
let mut paths = executor.block(client.fs.paths());
|
||||||
paths.remove(0);
|
paths.remove(0);
|
||||||
let new_root_path = if paths.is_empty() || self.rng.gen() {
|
let new_root_path = if paths.is_empty() || self.rng.gen() {
|
||||||
Path::new("/").join(&self.next_root_dir_name(user_id))
|
Path::new("/").join(&self.next_root_dir_name(user_id))
|
||||||
|
@ -1385,7 +1430,7 @@ impl TestPlan {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Query and mutate buffers
|
// Query and mutate buffers
|
||||||
60..=95 => {
|
60..=90 => {
|
||||||
let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
|
let Some(project) = choose_random_project(client, &mut self.rng) else { continue };
|
||||||
let project_root_name = root_name_for_project(&project, cx);
|
let project_root_name = root_name_for_project(&project, cx);
|
||||||
let is_local = project.read_with(cx, |project, _| project.is_local());
|
let is_local = project.read_with(cx, |project, _| project.is_local());
|
||||||
|
@ -1505,6 +1550,39 @@ impl TestPlan {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update a git index
|
||||||
|
91..=95 => {
|
||||||
|
let repo_path = executor
|
||||||
|
.block(client.fs.directories())
|
||||||
|
.choose(&mut self.rng)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let mut file_paths = executor
|
||||||
|
.block(client.fs.files())
|
||||||
|
.into_iter()
|
||||||
|
.filter(|path| path.starts_with(&repo_path))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let count = self.rng.gen_range(0..=file_paths.len());
|
||||||
|
file_paths.shuffle(&mut self.rng);
|
||||||
|
file_paths.truncate(count);
|
||||||
|
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
for abs_child_file_path in &file_paths {
|
||||||
|
let child_file_path = abs_child_file_path
|
||||||
|
.strip_prefix(&repo_path)
|
||||||
|
.unwrap()
|
||||||
|
.to_path_buf();
|
||||||
|
let new_base = Alphanumeric.sample_string(&mut self.rng, 16);
|
||||||
|
contents.push((child_file_path, new_base));
|
||||||
|
}
|
||||||
|
|
||||||
|
break ClientOperation::WriteGitIndex {
|
||||||
|
repo_path,
|
||||||
|
contents,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Create a file or directory
|
// Create a file or directory
|
||||||
96.. => {
|
96.. => {
|
||||||
let is_dir = self.rng.gen::<bool>();
|
let is_dir = self.rng.gen::<bool>();
|
||||||
|
|
|
@ -8,8 +8,10 @@ use fuzzy::{match_strings, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::{rect::RectF, vector::vec2f},
|
geometry::{rect::RectF, vector::vec2f},
|
||||||
impl_actions, impl_internal_actions, keymap, AppContext, CursorStyle, Entity, ModelHandle,
|
impl_actions, impl_internal_actions,
|
||||||
MouseButton, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
keymap_matcher::KeymapContext,
|
||||||
|
AppContext, CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext,
|
||||||
|
Subscription, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use menu::{Confirm, SelectNext, SelectPrev};
|
use menu::{Confirm, SelectNext, SelectPrev};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
@ -1267,7 +1269,7 @@ impl View for ContactList {
|
||||||
"ContactList"
|
"ContactList"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -3,7 +3,7 @@ use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::{ChildView, Flex, Label, ParentElement},
|
elements::{ChildView, Flex, Label, ParentElement},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
|
Action, AnyViewHandle, Element, Entity, MouseState, MutableAppContext, RenderContext, View,
|
||||||
ViewContext, ViewHandle,
|
ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
|
@ -64,8 +64,10 @@ impl CommandPalette {
|
||||||
name: humanize_action_name(name),
|
name: humanize_action_name(name),
|
||||||
action,
|
action,
|
||||||
keystrokes: bindings
|
keystrokes: bindings
|
||||||
|
.iter()
|
||||||
|
.filter_map(|binding| binding.keystrokes())
|
||||||
.last()
|
.last()
|
||||||
.map_or(Vec::new(), |binding| binding.keystrokes().to_vec()),
|
.map_or(Vec::new(), |keystrokes| keystrokes.to_vec()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap, platform::CursorStyle,
|
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
|
||||||
Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton, MutableAppContext, RenderContext,
|
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
|
||||||
SizeConstraint, Subscription, View, ViewContext,
|
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
|
||||||
};
|
};
|
||||||
use menu::*;
|
use menu::*;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -75,7 +75,7 @@ impl View for ContextMenu {
|
||||||
"ContextMenu"
|
"ContextMenu"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -36,6 +36,7 @@ use gpui::{
|
||||||
fonts::{self, HighlightStyle, TextStyle},
|
fonts::{self, HighlightStyle, TextStyle},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
|
keymap_matcher::KeymapContext,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
serde_json::json,
|
serde_json::json,
|
||||||
AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
|
||||||
|
@ -464,7 +465,7 @@ pub struct Editor {
|
||||||
searchable: bool,
|
searchable: bool,
|
||||||
cursor_shape: CursorShape,
|
cursor_shape: CursorShape,
|
||||||
workspace_id: Option<WorkspaceId>,
|
workspace_id: Option<WorkspaceId>,
|
||||||
keymap_context_layers: BTreeMap<TypeId, gpui::keymap::Context>,
|
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
|
||||||
input_enabled: bool,
|
input_enabled: bool,
|
||||||
leader_replica_id: Option<u16>,
|
leader_replica_id: Option<u16>,
|
||||||
remote_id: Option<ViewId>,
|
remote_id: Option<ViewId>,
|
||||||
|
@ -827,6 +828,23 @@ impl CompletionsMenu {
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Remove all candidates where the query's start does not match the start of any word in the candidate
|
||||||
|
if let Some(query) = query {
|
||||||
|
if let Some(query_start) = query.chars().next() {
|
||||||
|
matches.retain(|string_match| {
|
||||||
|
split_words(&string_match.string).any(|word| {
|
||||||
|
//Check that the first codepoint of the word as lowercase matches the first
|
||||||
|
//codepoint of the query as lowercase
|
||||||
|
word.chars()
|
||||||
|
.flat_map(|codepoint| codepoint.to_lowercase())
|
||||||
|
.zip(query_start.to_lowercase())
|
||||||
|
.all(|(word_cp, query_cp)| word_cp == query_cp)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
matches.sort_unstable_by_key(|mat| {
|
matches.sort_unstable_by_key(|mat| {
|
||||||
let completion = &self.completions[mat.candidate_id];
|
let completion = &self.completions[mat.candidate_id];
|
||||||
(
|
(
|
||||||
|
@ -1225,7 +1243,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: gpui::keymap::Context) {
|
pub fn set_keymap_context_layer<Tag: 'static>(&mut self, context: KeymapContext) {
|
||||||
self.keymap_context_layers
|
self.keymap_context_layers
|
||||||
.insert(TypeId::of::<Tag>(), context);
|
.insert(TypeId::of::<Tag>(), context);
|
||||||
}
|
}
|
||||||
|
@ -3611,7 +3629,9 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
|
||||||
|
dbg!("undo");
|
||||||
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
|
if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
|
||||||
|
dbg!(tx_id);
|
||||||
if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() {
|
if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() {
|
||||||
self.change_selections(None, cx, |s| {
|
self.change_selections(None, cx, |s| {
|
||||||
s.select_anchors(selections.to_vec());
|
s.select_anchors(selections.to_vec());
|
||||||
|
@ -6245,7 +6265,7 @@ impl View for Editor {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut context = Self::default_keymap_context();
|
let mut context = Self::default_keymap_context();
|
||||||
let mode = match self.mode {
|
let mode = match self.mode {
|
||||||
EditorMode::SingleLine => "single_line",
|
EditorMode::SingleLine => "single_line",
|
||||||
|
@ -6799,6 +6819,34 @@ pub fn styled_runs_for_code_label<'a>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
|
||||||
|
let mut index = 0;
|
||||||
|
let mut codepoints = text.char_indices().peekable();
|
||||||
|
|
||||||
|
std::iter::from_fn(move || {
|
||||||
|
let start_index = index;
|
||||||
|
while let Some((new_index, codepoint)) = codepoints.next() {
|
||||||
|
index = new_index + codepoint.len_utf8();
|
||||||
|
let current_upper = codepoint.is_uppercase();
|
||||||
|
let next_upper = codepoints
|
||||||
|
.peek()
|
||||||
|
.map(|(_, c)| c.is_uppercase())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !current_upper && next_upper {
|
||||||
|
return Some(&text[start_index..index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
index = text.len();
|
||||||
|
if start_index < text.len() {
|
||||||
|
return Some(&text[start_index..]);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.flat_map(|word| word.split_inclusive('_'))
|
||||||
|
}
|
||||||
|
|
||||||
trait RangeExt<T> {
|
trait RangeExt<T> {
|
||||||
fn sorted(&self) -> Range<T>;
|
fn sorted(&self) -> Range<T>;
|
||||||
fn to_inclusive(&self) -> RangeInclusive<T>;
|
fn to_inclusive(&self) -> RangeInclusive<T>;
|
||||||
|
|
|
@ -29,7 +29,11 @@ use workspace::{
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
fn test_edit_events(cx: &mut MutableAppContext) {
|
fn test_edit_events(cx: &mut MutableAppContext) {
|
||||||
cx.set_global(Settings::test(cx));
|
cx.set_global(Settings::test(cx));
|
||||||
let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx));
|
let buffer = cx.add_model(|cx| {
|
||||||
|
let mut buffer = language::Buffer::new(0, "123456", cx);
|
||||||
|
buffer.set_group_interval(Duration::from_secs(1));
|
||||||
|
buffer
|
||||||
|
});
|
||||||
|
|
||||||
let events = Rc::new(RefCell::new(Vec::new()));
|
let events = Rc::new(RefCell::new(Vec::new()));
|
||||||
let (_, editor1) = cx.add_window(Default::default(), {
|
let (_, editor1) = cx.add_window(Default::default(), {
|
||||||
|
@ -3502,6 +3506,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
view.undo(&Undo, cx);
|
||||||
|
view.undo(&Undo, cx);
|
||||||
view.undo(&Undo, cx);
|
view.undo(&Undo, cx);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
view.text(cx),
|
view.text(cx),
|
||||||
|
@ -5439,6 +5445,20 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_split_words() {
|
||||||
|
fn split<'a>(text: &'a str) -> Vec<&'a str> {
|
||||||
|
split_words(text).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(split("HelloWorld"), &["Hello", "World"]);
|
||||||
|
assert_eq!(split("hello_world"), &["hello_", "world"]);
|
||||||
|
assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
|
||||||
|
assert_eq!(split("Hello_World"), &["Hello_", "World"]);
|
||||||
|
assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
|
||||||
|
assert_eq!(split("helloworld"), &["helloworld"]);
|
||||||
|
}
|
||||||
|
|
||||||
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(row as u32, column as u32);
|
let point = DisplayPoint::new(row as u32, column as u32);
|
||||||
point..point
|
point..point
|
||||||
|
|
|
@ -9,7 +9,9 @@ use indoc::indoc;
|
||||||
use crate::{
|
use crate::{
|
||||||
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
|
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
|
||||||
};
|
};
|
||||||
use gpui::{keymap::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle};
|
use gpui::{
|
||||||
|
keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle,
|
||||||
|
};
|
||||||
use language::{Buffer, BufferSnapshot};
|
use language::{Buffer, BufferSnapshot};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use util::{
|
use util::{
|
||||||
|
|
|
@ -35,7 +35,7 @@ use repository::FakeGitRepositoryState;
|
||||||
use std::sync::Weak;
|
use std::sync::Weak;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref CARRIAGE_RETURNS_REGEX: Regex = Regex::new("\r\n|\r").unwrap();
|
static ref LINE_SEPERATORS_REGEX: Regex = Regex::new("\r\n|\r|\u{2028}|\u{2029}").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
@ -80,13 +80,13 @@ impl LineEnding {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalize(text: &mut String) {
|
pub fn normalize(text: &mut String) {
|
||||||
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(text, "\n") {
|
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(text, "\n") {
|
||||||
*text = replaced;
|
*text = replaced;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
pub fn normalize_arc(text: Arc<str>) -> Arc<str> {
|
||||||
if let Cow::Owned(replaced) = CARRIAGE_RETURNS_REGEX.replace_all(&text, "\n") {
|
if let Cow::Owned(replaced) = LINE_SEPERATORS_REGEX.replace_all(&text, "\n") {
|
||||||
replaced.into()
|
replaced.into()
|
||||||
} else {
|
} else {
|
||||||
text
|
text
|
||||||
|
|
|
@ -52,7 +52,7 @@ fn compile_metal_shaders() {
|
||||||
println!("cargo:rerun-if-changed={}", shader_path);
|
println!("cargo:rerun-if-changed={}", shader_path);
|
||||||
|
|
||||||
let output = Command::new("xcrun")
|
let output = Command::new("xcrun")
|
||||||
.args(&[
|
.args([
|
||||||
"-sdk",
|
"-sdk",
|
||||||
"macosx",
|
"macosx",
|
||||||
"metal",
|
"metal",
|
||||||
|
@ -76,7 +76,7 @@ fn compile_metal_shaders() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = Command::new("xcrun")
|
let output = Command::new("xcrun")
|
||||||
.args(&["-sdk", "macosx", "metallib"])
|
.args(["-sdk", "macosx", "metallib"])
|
||||||
.arg(air_output_path)
|
.arg(air_output_path)
|
||||||
.arg("-o")
|
.arg("-o")
|
||||||
.arg(metallib_output_path)
|
.arg(metallib_output_path)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,19 +1,44 @@
|
||||||
|
use crate::MutableAppContext;
|
||||||
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{hash::Hash, sync::Weak};
|
use std::{hash::Hash, sync::Weak};
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
pub struct CallbackCollection<K: Clone + Hash + Eq, F> {
|
||||||
|
internal: Arc<Mutex<Mapping<K, F>>>,
|
||||||
use collections::{btree_map, BTreeMap, HashMap};
|
|
||||||
|
|
||||||
use crate::MutableAppContext;
|
|
||||||
|
|
||||||
pub type Mapping<K, F> = Mutex<HashMap<K, BTreeMap<usize, Option<F>>>>;
|
|
||||||
|
|
||||||
pub struct CallbackCollection<K: Hash + Eq, F> {
|
|
||||||
internal: Arc<Mapping<K, F>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
|
pub struct Subscription<K: Clone + Hash + Eq, F> {
|
||||||
|
key: K,
|
||||||
|
id: usize,
|
||||||
|
mapping: Option<Weak<Mutex<Mapping<K, F>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Mapping<K, F> {
|
||||||
|
callbacks: HashMap<K, BTreeMap<usize, F>>,
|
||||||
|
dropped_subscriptions: HashMap<K, HashSet<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Hash + Eq, F> Mapping<K, F> {
|
||||||
|
fn clear_dropped_state(&mut self, key: &K, subscription_id: usize) -> bool {
|
||||||
|
if let Some(subscriptions) = self.dropped_subscriptions.get_mut(&key) {
|
||||||
|
subscriptions.remove(&subscription_id)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, F> Default for Mapping<K, F> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
callbacks: Default::default(),
|
||||||
|
dropped_subscriptions: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Clone + Hash + Eq, F> Clone for CallbackCollection<K, F> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
internal: self.internal.clone(),
|
internal: self.internal.clone(),
|
||||||
|
@ -21,7 +46,7 @@ impl<K: Hash + Eq, F> Clone for CallbackCollection<K, F> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
|
impl<K: Clone + Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
CallbackCollection {
|
CallbackCollection {
|
||||||
internal: Arc::new(Mutex::new(Default::default())),
|
internal: Arc::new(Mutex::new(Default::default())),
|
||||||
|
@ -29,78 +54,114 @@ impl<K: Hash + Eq + Copy, F> Default for CallbackCollection<K, F> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Hash + Eq + Copy, F> CallbackCollection<K, F> {
|
impl<K: Clone + Hash + Eq + Copy, F> CallbackCollection<K, F> {
|
||||||
pub fn downgrade(&self) -> Weak<Mapping<K, F>> {
|
|
||||||
Arc::downgrade(&self.internal)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.internal.lock().is_empty()
|
self.internal.lock().callbacks.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_callback(&mut self, id: K, subscription_id: usize, callback: F) {
|
pub fn subscribe(&mut self, key: K, subscription_id: usize) -> Subscription<K, F> {
|
||||||
self.internal
|
Subscription {
|
||||||
.lock()
|
key,
|
||||||
.entry(id)
|
id: subscription_id,
|
||||||
|
mapping: Some(Arc::downgrade(&self.internal)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_callback(&mut self, key: K, subscription_id: usize, callback: F) {
|
||||||
|
let mut this = self.internal.lock();
|
||||||
|
|
||||||
|
// If this callback's subscription was dropped before the callback was
|
||||||
|
// added, then just drop the callback.
|
||||||
|
if this.clear_dropped_state(&key, subscription_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks
|
||||||
|
.entry(key)
|
||||||
.or_default()
|
.or_default()
|
||||||
.insert(subscription_id, Some(callback));
|
.insert(subscription_id, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, id: K) {
|
pub fn remove(&mut self, key: K) {
|
||||||
self.internal.lock().remove(&id);
|
// Drop these callbacks after releasing the lock, in case one of them
|
||||||
|
// owns a subscription to this callback collection.
|
||||||
|
let mut this = self.internal.lock();
|
||||||
|
let callbacks = this.callbacks.remove(&key);
|
||||||
|
this.dropped_subscriptions.remove(&key);
|
||||||
|
drop(this);
|
||||||
|
drop(callbacks);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_or_remove_callback(&mut self, id: K, subscription_id: usize, callback: F) {
|
pub fn emit<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
|
||||||
match self
|
|
||||||
.internal
|
|
||||||
.lock()
|
|
||||||
.entry(id)
|
|
||||||
.or_default()
|
|
||||||
.entry(subscription_id)
|
|
||||||
{
|
|
||||||
btree_map::Entry::Vacant(entry) => {
|
|
||||||
entry.insert(Some(callback));
|
|
||||||
}
|
|
||||||
|
|
||||||
btree_map::Entry::Occupied(entry) => {
|
|
||||||
// TODO: This seems like it should never be called because no code
|
|
||||||
// should ever attempt to remove an existing callback
|
|
||||||
debug_assert!(entry.get().is_none());
|
|
||||||
entry.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn emit_and_cleanup<C: FnMut(&mut F, &mut MutableAppContext) -> bool>(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
id: K,
|
key: K,
|
||||||
cx: &mut MutableAppContext,
|
cx: &mut MutableAppContext,
|
||||||
mut call_callback: C,
|
mut call_callback: C,
|
||||||
) {
|
) {
|
||||||
let callbacks = self.internal.lock().remove(&id);
|
let callbacks = self.internal.lock().callbacks.remove(&key);
|
||||||
if let Some(callbacks) = callbacks {
|
if let Some(callbacks) = callbacks {
|
||||||
for (subscription_id, callback) in callbacks {
|
for (subscription_id, mut callback) in callbacks {
|
||||||
if let Some(mut callback) = callback {
|
// If this callback's subscription was dropped while invoking an
|
||||||
|
// earlier callback, then just drop the callback.
|
||||||
|
let mut this = self.internal.lock();
|
||||||
|
if this.clear_dropped_state(&key, subscription_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(this);
|
||||||
let alive = call_callback(&mut callback, cx);
|
let alive = call_callback(&mut callback, cx);
|
||||||
if alive {
|
|
||||||
match self
|
// If this callback's subscription was dropped while invoking the callback
|
||||||
.internal
|
// itself, or if the callback returns false, then just drop the callback.
|
||||||
.lock()
|
let mut this = self.internal.lock();
|
||||||
.entry(id)
|
if this.clear_dropped_state(&key, subscription_id) || !alive {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.callbacks
|
||||||
|
.entry(key)
|
||||||
.or_default()
|
.or_default()
|
||||||
.entry(subscription_id)
|
.insert(subscription_id, callback);
|
||||||
{
|
|
||||||
btree_map::Entry::Vacant(entry) => {
|
|
||||||
entry.insert(Some(callback));
|
|
||||||
}
|
|
||||||
btree_map::Entry::Occupied(entry) => {
|
|
||||||
entry.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<K: Clone + Hash + Eq, F> Subscription<K, F> {
|
||||||
|
pub fn id(&self) -> usize {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn detach(&mut self) {
|
||||||
|
self.mapping.take();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Clone + Hash + Eq, F> Drop for Subscription<K, F> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(mapping) = self.mapping.as_ref().and_then(|mapping| mapping.upgrade()) {
|
||||||
|
let mut mapping = mapping.lock();
|
||||||
|
|
||||||
|
// If the callback is present in the mapping, then just remove it.
|
||||||
|
if let Some(callbacks) = mapping.callbacks.get_mut(&self.key) {
|
||||||
|
let callback = callbacks.remove(&self.id);
|
||||||
|
if callback.is_some() {
|
||||||
|
drop(mapping);
|
||||||
|
drop(callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this subscription's callback is not present, then either it has been
|
||||||
|
// temporarily removed during emit, or it has not yet been added. Record
|
||||||
|
// that this subscription has been dropped so that the callback can be
|
||||||
|
// removed later.
|
||||||
|
mapping
|
||||||
|
.dropped_subscriptions
|
||||||
|
.entry(self.key.clone())
|
||||||
|
.or_default()
|
||||||
|
.insert(self.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -17,11 +17,11 @@ use parking_lot::{Mutex, RwLock};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, geometry::vector::Vector2F, keymap::Keystroke, platform, Action, AnyViewHandle,
|
executor, geometry::vector::Vector2F, keymap_matcher::Keystroke, platform, Action,
|
||||||
AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent, LeakDetector,
|
AnyViewHandle, AppContext, Appearance, Entity, Event, FontCache, InputHandler, KeyDownEvent,
|
||||||
ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith, ReadViewWith,
|
LeakDetector, ModelContext, ModelHandle, MutableAppContext, Platform, ReadModelWith,
|
||||||
RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle, WeakHandle,
|
ReadViewWith, RenderContext, Task, UpdateModel, UpdateView, View, ViewContext, ViewHandle,
|
||||||
WindowInputHandler,
|
WeakHandle, WindowInputHandler,
|
||||||
};
|
};
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub mod executor;
|
||||||
pub use executor::Task;
|
pub use executor::Task;
|
||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
pub mod keymap;
|
pub mod keymap_matcher;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub use gpui_macros::test;
|
pub use gpui_macros::test;
|
||||||
pub use platform::*;
|
pub use platform::*;
|
||||||
|
|
|
@ -1,757 +0,0 @@
|
||||||
use crate::Action;
|
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use std::{
|
|
||||||
any::{Any, TypeId},
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
fmt::{Debug, Write},
|
|
||||||
};
|
|
||||||
use tree_sitter::{Language, Node, Parser};
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
fn tree_sitter_context_predicate() -> Language;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Matcher {
|
|
||||||
pending_views: HashMap<usize, Context>,
|
|
||||||
pending_keystrokes: Vec<Keystroke>,
|
|
||||||
keymap: Keymap,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Keymap {
|
|
||||||
bindings: Vec<Binding>,
|
|
||||||
binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Binding {
|
|
||||||
keystrokes: SmallVec<[Keystroke; 2]>,
|
|
||||||
action: Box<dyn Action>,
|
|
||||||
context_predicate: Option<ContextPredicate>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
pub struct Keystroke {
|
|
||||||
pub ctrl: bool,
|
|
||||||
pub alt: bool,
|
|
||||||
pub shift: bool,
|
|
||||||
pub cmd: bool,
|
|
||||||
pub function: bool,
|
|
||||||
pub key: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
|
||||||
pub struct Context {
|
|
||||||
pub set: HashSet<String>,
|
|
||||||
pub map: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
|
||||||
enum ContextPredicate {
|
|
||||||
Identifier(String),
|
|
||||||
Equal(String, String),
|
|
||||||
NotEqual(String, String),
|
|
||||||
Not(Box<ContextPredicate>),
|
|
||||||
And(Box<ContextPredicate>, Box<ContextPredicate>),
|
|
||||||
Or(Box<ContextPredicate>, Box<ContextPredicate>),
|
|
||||||
}
|
|
||||||
|
|
||||||
trait ActionArg {
|
|
||||||
fn boxed_clone(&self) -> Box<dyn Any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> ActionArg for T
|
|
||||||
where
|
|
||||||
T: 'static + Any + Clone,
|
|
||||||
{
|
|
||||||
fn boxed_clone(&self) -> Box<dyn Any> {
|
|
||||||
Box::new(self.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum MatchResult {
|
|
||||||
None,
|
|
||||||
Pending,
|
|
||||||
Matches(Vec<(usize, Box<dyn Action>)>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for MatchResult {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
MatchResult::None => f.debug_struct("MatchResult::None").finish(),
|
|
||||||
MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
|
|
||||||
MatchResult::Matches(matches) => f
|
|
||||||
.debug_list()
|
|
||||||
.entries(
|
|
||||||
matches
|
|
||||||
.iter()
|
|
||||||
.map(|(view_id, action)| format!("{view_id}, {}", action.name())),
|
|
||||||
)
|
|
||||||
.finish(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for MatchResult {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
match (self, other) {
|
|
||||||
(MatchResult::None, MatchResult::None) => true,
|
|
||||||
(MatchResult::Pending, MatchResult::Pending) => true,
|
|
||||||
(MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
|
|
||||||
matches.len() == other_matches.len()
|
|
||||||
&& matches.iter().zip(other_matches.iter()).all(
|
|
||||||
|((view_id, action), (other_view_id, other_action))| {
|
|
||||||
view_id == other_view_id && action.eq(other_action.as_ref())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for MatchResult {}
|
|
||||||
|
|
||||||
impl Clone for MatchResult {
|
|
||||||
fn clone(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
MatchResult::None => MatchResult::None,
|
|
||||||
MatchResult::Pending => MatchResult::Pending,
|
|
||||||
MatchResult::Matches(matches) => MatchResult::Matches(
|
|
||||||
matches
|
|
||||||
.iter()
|
|
||||||
.map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
|
|
||||||
.collect(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Matcher {
|
|
||||||
pub fn new(keymap: Keymap) -> Self {
|
|
||||||
Self {
|
|
||||||
pending_views: HashMap::new(),
|
|
||||||
pending_keystrokes: Vec::new(),
|
|
||||||
keymap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_keymap(&mut self, keymap: Keymap) {
|
|
||||||
self.clear_pending();
|
|
||||||
self.keymap = keymap;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
|
||||||
self.clear_pending();
|
|
||||||
self.keymap.add_bindings(bindings);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_bindings(&mut self) {
|
|
||||||
self.clear_pending();
|
|
||||||
self.keymap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
|
|
||||||
self.keymap.bindings_for_action_type(action_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_pending(&mut self) {
|
|
||||||
self.pending_keystrokes.clear();
|
|
||||||
self.pending_views.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_pending_keystrokes(&self) -> bool {
|
|
||||||
!self.pending_keystrokes.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push_keystroke(
|
|
||||||
&mut self,
|
|
||||||
keystroke: Keystroke,
|
|
||||||
dispatch_path: Vec<(usize, Context)>,
|
|
||||||
) -> MatchResult {
|
|
||||||
let mut any_pending = false;
|
|
||||||
let mut matched_bindings = Vec::new();
|
|
||||||
|
|
||||||
let first_keystroke = self.pending_keystrokes.is_empty();
|
|
||||||
self.pending_keystrokes.push(keystroke);
|
|
||||||
|
|
||||||
for (view_id, context) in dispatch_path {
|
|
||||||
// Don't require pending view entry if there are no pending keystrokes
|
|
||||||
if !first_keystroke && !self.pending_views.contains_key(&view_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there is a previous view context, invalidate that view if it
|
|
||||||
// has changed
|
|
||||||
if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
|
|
||||||
if previous_view_context != context {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the bindings which map the pending keystrokes and current context
|
|
||||||
for binding in self.keymap.bindings.iter().rev() {
|
|
||||||
if binding.keystrokes.starts_with(&self.pending_keystrokes)
|
|
||||||
&& binding
|
|
||||||
.context_predicate
|
|
||||||
.as_ref()
|
|
||||||
.map(|c| c.eval(&context))
|
|
||||||
.unwrap_or(true)
|
|
||||||
{
|
|
||||||
// If the binding is completed, push it onto the matches list
|
|
||||||
if binding.keystrokes.len() == self.pending_keystrokes.len() {
|
|
||||||
matched_bindings.push((view_id, binding.action.boxed_clone()));
|
|
||||||
} else {
|
|
||||||
// Otherwise, the binding is still pending
|
|
||||||
self.pending_views.insert(view_id, context.clone());
|
|
||||||
any_pending = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !any_pending {
|
|
||||||
self.clear_pending();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !matched_bindings.is_empty() {
|
|
||||||
MatchResult::Matches(matched_bindings)
|
|
||||||
} else if any_pending {
|
|
||||||
MatchResult::Pending
|
|
||||||
} else {
|
|
||||||
MatchResult::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keystrokes_for_action(
|
|
||||||
&self,
|
|
||||||
action: &dyn Action,
|
|
||||||
cx: &Context,
|
|
||||||
) -> Option<SmallVec<[Keystroke; 2]>> {
|
|
||||||
for binding in self.keymap.bindings.iter().rev() {
|
|
||||||
if binding.action.eq(action)
|
|
||||||
&& binding
|
|
||||||
.context_predicate
|
|
||||||
.as_ref()
|
|
||||||
.map_or(true, |predicate| predicate.eval(cx))
|
|
||||||
{
|
|
||||||
return Some(binding.keystrokes.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Matcher {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new(Keymap::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Keymap {
|
|
||||||
pub fn new(bindings: Vec<Binding>) -> Self {
|
|
||||||
let mut binding_indices_by_action_type = HashMap::new();
|
|
||||||
for (ix, binding) in bindings.iter().enumerate() {
|
|
||||||
binding_indices_by_action_type
|
|
||||||
.entry(binding.action.as_any().type_id())
|
|
||||||
.or_insert_with(SmallVec::new)
|
|
||||||
.push(ix);
|
|
||||||
}
|
|
||||||
Self {
|
|
||||||
binding_indices_by_action_type,
|
|
||||||
bindings,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &'_ Binding> {
|
|
||||||
self.binding_indices_by_action_type
|
|
||||||
.get(&action_type)
|
|
||||||
.map(SmallVec::as_slice)
|
|
||||||
.unwrap_or(&[])
|
|
||||||
.iter()
|
|
||||||
.map(|ix| &self.bindings[*ix])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
|
||||||
for binding in bindings {
|
|
||||||
self.binding_indices_by_action_type
|
|
||||||
.entry(binding.action.as_any().type_id())
|
|
||||||
.or_default()
|
|
||||||
.push(self.bindings.len());
|
|
||||||
self.bindings.push(binding);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clear(&mut self) {
|
|
||||||
self.bindings.clear();
|
|
||||||
self.binding_indices_by_action_type.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Binding {
|
|
||||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
|
||||||
Self::load(keystrokes, Box::new(action), context).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
|
||||||
let context = if let Some(context) = context {
|
|
||||||
Some(ContextPredicate::parse(context)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let keystrokes = keystrokes
|
|
||||||
.split_whitespace()
|
|
||||||
.map(Keystroke::parse)
|
|
||||||
.collect::<Result<_>>()?;
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
keystrokes,
|
|
||||||
action,
|
|
||||||
context_predicate: context,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
|
||||||
&self.keystrokes
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn action(&self) -> &dyn Action {
|
|
||||||
self.action.as_ref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Keystroke {
|
|
||||||
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
|
||||||
let mut ctrl = false;
|
|
||||||
let mut alt = false;
|
|
||||||
let mut shift = false;
|
|
||||||
let mut cmd = false;
|
|
||||||
let mut function = false;
|
|
||||||
let mut key = None;
|
|
||||||
|
|
||||||
let mut components = source.split('-').peekable();
|
|
||||||
while let Some(component) = components.next() {
|
|
||||||
match component {
|
|
||||||
"ctrl" => ctrl = true,
|
|
||||||
"alt" => alt = true,
|
|
||||||
"shift" => shift = true,
|
|
||||||
"cmd" => cmd = true,
|
|
||||||
"fn" => function = true,
|
|
||||||
_ => {
|
|
||||||
if let Some(component) = components.peek() {
|
|
||||||
if component.is_empty() && source.ends_with('-') {
|
|
||||||
key = Some(String::from("-"));
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
return Err(anyhow!("Invalid keystroke `{}`", source));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
key = Some(String::from(component));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
|
||||||
|
|
||||||
Ok(Keystroke {
|
|
||||||
ctrl,
|
|
||||||
alt,
|
|
||||||
shift,
|
|
||||||
cmd,
|
|
||||||
function,
|
|
||||||
key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn modified(&self) -> bool {
|
|
||||||
self.ctrl || self.alt || self.shift || self.cmd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Keystroke {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
if self.ctrl {
|
|
||||||
f.write_char('^')?;
|
|
||||||
}
|
|
||||||
if self.alt {
|
|
||||||
f.write_char('⎇')?;
|
|
||||||
}
|
|
||||||
if self.cmd {
|
|
||||||
f.write_char('⌘')?;
|
|
||||||
}
|
|
||||||
if self.shift {
|
|
||||||
f.write_char('⇧')?;
|
|
||||||
}
|
|
||||||
let key = match self.key.as_str() {
|
|
||||||
"backspace" => '⌫',
|
|
||||||
"up" => '↑',
|
|
||||||
"down" => '↓',
|
|
||||||
"left" => '←',
|
|
||||||
"right" => '→',
|
|
||||||
"tab" => '⇥',
|
|
||||||
"escape" => '⎋',
|
|
||||||
key => {
|
|
||||||
if key.len() == 1 {
|
|
||||||
key.chars().next().unwrap().to_ascii_uppercase()
|
|
||||||
} else {
|
|
||||||
return f.write_str(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
f.write_char(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Context {
|
|
||||||
pub fn extend(&mut self, other: &Context) {
|
|
||||||
for v in &other.set {
|
|
||||||
self.set.insert(v.clone());
|
|
||||||
}
|
|
||||||
for (k, v) in &other.map {
|
|
||||||
self.map.insert(k.clone(), v.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContextPredicate {
|
|
||||||
fn parse(source: &str) -> anyhow::Result<Self> {
|
|
||||||
let mut parser = Parser::new();
|
|
||||||
let language = unsafe { tree_sitter_context_predicate() };
|
|
||||||
parser.set_language(language).unwrap();
|
|
||||||
let source = source.as_bytes();
|
|
||||||
let tree = parser.parse(source, None).unwrap();
|
|
||||||
Self::from_node(tree.root_node(), source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
|
|
||||||
let parse_error = "error parsing context predicate";
|
|
||||||
let kind = node.kind();
|
|
||||||
|
|
||||||
match kind {
|
|
||||||
"source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
|
|
||||||
"identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
|
|
||||||
"not" => {
|
|
||||||
let child = Self::from_node(
|
|
||||||
node.child_by_field_name("expression")
|
|
||||||
.ok_or_else(|| anyhow!(parse_error))?,
|
|
||||||
source,
|
|
||||||
)?;
|
|
||||||
Ok(Self::Not(Box::new(child)))
|
|
||||||
}
|
|
||||||
"and" | "or" => {
|
|
||||||
let left = Box::new(Self::from_node(
|
|
||||||
node.child_by_field_name("left")
|
|
||||||
.ok_or_else(|| anyhow!(parse_error))?,
|
|
||||||
source,
|
|
||||||
)?);
|
|
||||||
let right = Box::new(Self::from_node(
|
|
||||||
node.child_by_field_name("right")
|
|
||||||
.ok_or_else(|| anyhow!(parse_error))?,
|
|
||||||
source,
|
|
||||||
)?);
|
|
||||||
if kind == "and" {
|
|
||||||
Ok(Self::And(left, right))
|
|
||||||
} else {
|
|
||||||
Ok(Self::Or(left, right))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"equal" | "not_equal" => {
|
|
||||||
let left = node
|
|
||||||
.child_by_field_name("left")
|
|
||||||
.ok_or_else(|| anyhow!(parse_error))?
|
|
||||||
.utf8_text(source)?
|
|
||||||
.into();
|
|
||||||
let right = node
|
|
||||||
.child_by_field_name("right")
|
|
||||||
.ok_or_else(|| anyhow!(parse_error))?
|
|
||||||
.utf8_text(source)?
|
|
||||||
.into();
|
|
||||||
if kind == "equal" {
|
|
||||||
Ok(Self::Equal(left, right))
|
|
||||||
} else {
|
|
||||||
Ok(Self::NotEqual(left, right))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"parenthesized" => Self::from_node(
|
|
||||||
node.child_by_field_name("expression")
|
|
||||||
.ok_or_else(|| anyhow!(parse_error))?,
|
|
||||||
source,
|
|
||||||
),
|
|
||||||
_ => Err(anyhow!(parse_error)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eval(&self, cx: &Context) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Identifier(name) => cx.set.contains(name.as_str()),
|
|
||||||
Self::Equal(left, right) => cx
|
|
||||||
.map
|
|
||||||
.get(left)
|
|
||||||
.map(|value| value == right)
|
|
||||||
.unwrap_or(false),
|
|
||||||
Self::NotEqual(left, right) => {
|
|
||||||
cx.map.get(left).map(|value| value != right).unwrap_or(true)
|
|
||||||
}
|
|
||||||
Self::Not(pred) => !pred.eval(cx),
|
|
||||||
Self::And(left, right) => left.eval(cx) && right.eval(cx),
|
|
||||||
Self::Or(left, right) => left.eval(cx) || right.eval(cx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use anyhow::Result;
|
|
||||||
use serde::Deserialize;
|
|
||||||
|
|
||||||
use crate::{actions, impl_actions};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_push_keystroke() -> Result<()> {
|
|
||||||
actions!(test, [B, AB, C, D, DA]);
|
|
||||||
|
|
||||||
let mut ctx1 = Context::default();
|
|
||||||
ctx1.set.insert("1".into());
|
|
||||||
|
|
||||||
let mut ctx2 = Context::default();
|
|
||||||
ctx2.set.insert("2".into());
|
|
||||||
|
|
||||||
let dispatch_path = vec![(2, ctx2), (1, ctx1)];
|
|
||||||
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
Binding::new("a b", AB, Some("1")),
|
|
||||||
Binding::new("b", B, Some("2")),
|
|
||||||
Binding::new("c", C, Some("2")),
|
|
||||||
Binding::new("d", D, Some("1")),
|
|
||||||
Binding::new("d", D, Some("2")),
|
|
||||||
Binding::new("d a", DA, Some("2")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut matcher = Matcher::new(keymap);
|
|
||||||
|
|
||||||
// Binding with pending prefix always takes precedence
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Pending,
|
|
||||||
);
|
|
||||||
// B alone doesn't match because a was pending, so AB is returned instead
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(AB))]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// Without an a prefix, B is dispatched like expected
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(2, Box::new(B))]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// If a is prefixed, C will not be dispatched because there
|
|
||||||
// was a pending binding for it
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Pending,
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
|
|
||||||
MatchResult::None,
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
// If a single keystroke matches multiple bindings in the tree
|
|
||||||
// all of them are returned so that we can fallback if the action
|
|
||||||
// handler decides to propagate the action
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
|
|
||||||
);
|
|
||||||
// If none of the d action handlers consume the binding, a pending
|
|
||||||
// binding may then be used
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
|
||||||
MatchResult::Matches(vec![(2, Box::new(DA))]),
|
|
||||||
);
|
|
||||||
assert!(!matcher.has_pending_keystrokes());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_keystroke_parsing() -> Result<()> {
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("ctrl-p")?,
|
|
||||||
Keystroke {
|
|
||||||
key: "p".into(),
|
|
||||||
ctrl: true,
|
|
||||||
alt: false,
|
|
||||||
shift: false,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("alt-shift-down")?,
|
|
||||||
Keystroke {
|
|
||||||
key: "down".into(),
|
|
||||||
ctrl: false,
|
|
||||||
alt: true,
|
|
||||||
shift: true,
|
|
||||||
cmd: false,
|
|
||||||
function: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
Keystroke::parse("shift-cmd--")?,
|
|
||||||
Keystroke {
|
|
||||||
key: "-".into(),
|
|
||||||
ctrl: false,
|
|
||||||
alt: false,
|
|
||||||
shift: true,
|
|
||||||
cmd: true,
|
|
||||||
function: false,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_predicate_parsing() -> Result<()> {
|
|
||||||
use ContextPredicate::*;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ContextPredicate::parse("a && (b == c || d != e)")?,
|
|
||||||
And(
|
|
||||||
Box::new(Identifier("a".into())),
|
|
||||||
Box::new(Or(
|
|
||||||
Box::new(Equal("b".into(), "c".into())),
|
|
||||||
Box::new(NotEqual("d".into(), "e".into())),
|
|
||||||
))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ContextPredicate::parse("!a")?,
|
|
||||||
Not(Box::new(Identifier("a".into())),)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_context_predicate_eval() -> Result<()> {
|
|
||||||
let predicate = ContextPredicate::parse("a && b || c == d")?;
|
|
||||||
|
|
||||||
let mut context = Context::default();
|
|
||||||
context.set.insert("a".into());
|
|
||||||
assert!(!predicate.eval(&context));
|
|
||||||
|
|
||||||
context.set.insert("b".into());
|
|
||||||
assert!(predicate.eval(&context));
|
|
||||||
|
|
||||||
context.set.remove("b");
|
|
||||||
context.map.insert("c".into(), "x".into());
|
|
||||||
assert!(!predicate.eval(&context));
|
|
||||||
|
|
||||||
context.map.insert("c".into(), "d".into());
|
|
||||||
assert!(predicate.eval(&context));
|
|
||||||
|
|
||||||
let predicate = ContextPredicate::parse("!a")?;
|
|
||||||
assert!(predicate.eval(&Context::default()));
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_matcher() -> Result<()> {
|
|
||||||
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
|
|
||||||
pub struct A(pub String);
|
|
||||||
impl_actions!(test, [A]);
|
|
||||||
actions!(test, [B, Ab]);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
||||||
struct ActionArg {
|
|
||||||
a: &'static str,
|
|
||||||
}
|
|
||||||
|
|
||||||
let keymap = Keymap::new(vec![
|
|
||||||
Binding::new("a", A("x".to_string()), Some("a")),
|
|
||||||
Binding::new("b", B, Some("a")),
|
|
||||||
Binding::new("a b", Ab, Some("a || b")),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let mut ctx_a = Context::default();
|
|
||||||
ctx_a.set.insert("a".into());
|
|
||||||
|
|
||||||
let mut ctx_b = Context::default();
|
|
||||||
ctx_b.set.insert("b".into());
|
|
||||||
|
|
||||||
let mut matcher = Matcher::new(keymap);
|
|
||||||
|
|
||||||
// Basic match
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Multi-keystroke match
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
|
|
||||||
MatchResult::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Ab))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Failed matches don't interfere with matching subsequent keys
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, ctx_a.clone())]),
|
|
||||||
MatchResult::None
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_a.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
// Pending keystrokes are cleared when the context changes
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, ctx_b.clone())]),
|
|
||||||
MatchResult::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_a.clone())]),
|
|
||||||
MatchResult::None
|
|
||||||
);
|
|
||||||
matcher.clear_pending();
|
|
||||||
|
|
||||||
let mut ctx_c = Context::default();
|
|
||||||
ctx_c.set.insert("c".into());
|
|
||||||
|
|
||||||
// Pending keystrokes are maintained per-view
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(
|
|
||||||
Keystroke::parse("a")?,
|
|
||||||
vec![(1, ctx_b.clone()), (2, ctx_c.clone())]
|
|
||||||
),
|
|
||||||
MatchResult::Pending
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, ctx_b.clone())]),
|
|
||||||
MatchResult::Matches(vec![(1, Box::new(Ab))])
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
459
crates/gpui/src/keymap_matcher.rs
Normal file
459
crates/gpui/src/keymap_matcher.rs
Normal file
|
@ -0,0 +1,459 @@
|
||||||
|
mod binding;
|
||||||
|
mod keymap;
|
||||||
|
mod keymap_context;
|
||||||
|
mod keystroke;
|
||||||
|
|
||||||
|
use std::{any::TypeId, fmt::Debug};
|
||||||
|
|
||||||
|
use collections::HashMap;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::{impl_actions, Action};
|
||||||
|
|
||||||
|
pub use binding::{Binding, BindingMatchResult};
|
||||||
|
pub use keymap::Keymap;
|
||||||
|
pub use keymap_context::{KeymapContext, KeymapContextPredicate};
|
||||||
|
pub use keystroke::Keystroke;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize)]
|
||||||
|
pub struct KeyPressed {
|
||||||
|
#[serde(default)]
|
||||||
|
pub keystroke: Keystroke,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_actions!(gpui, [KeyPressed]);
|
||||||
|
|
||||||
|
pub struct KeymapMatcher {
|
||||||
|
pending_views: HashMap<usize, KeymapContext>,
|
||||||
|
pending_keystrokes: Vec<Keystroke>,
|
||||||
|
keymap: Keymap,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeymapMatcher {
|
||||||
|
pub fn new(keymap: Keymap) -> Self {
|
||||||
|
Self {
|
||||||
|
pending_views: Default::default(),
|
||||||
|
pending_keystrokes: Vec::new(),
|
||||||
|
keymap,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_keymap(&mut self, keymap: Keymap) {
|
||||||
|
self.clear_pending();
|
||||||
|
self.keymap = keymap;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
||||||
|
self.clear_pending();
|
||||||
|
self.keymap.add_bindings(bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_bindings(&mut self) {
|
||||||
|
self.clear_pending();
|
||||||
|
self.keymap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bindings_for_action_type(&self, action_type: TypeId) -> impl Iterator<Item = &Binding> {
|
||||||
|
self.keymap.bindings_for_action_type(action_type)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_pending(&mut self) {
|
||||||
|
self.pending_keystrokes.clear();
|
||||||
|
self.pending_views.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_pending_keystrokes(&self) -> bool {
|
||||||
|
!self.pending_keystrokes.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push_keystroke(
|
||||||
|
&mut self,
|
||||||
|
keystroke: Keystroke,
|
||||||
|
dispatch_path: Vec<(usize, KeymapContext)>,
|
||||||
|
) -> MatchResult {
|
||||||
|
let mut any_pending = false;
|
||||||
|
let mut matched_bindings: Vec<(usize, Box<dyn Action>)> = Vec::new();
|
||||||
|
|
||||||
|
let first_keystroke = self.pending_keystrokes.is_empty();
|
||||||
|
self.pending_keystrokes.push(keystroke.clone());
|
||||||
|
|
||||||
|
for (view_id, context) in dispatch_path {
|
||||||
|
// Don't require pending view entry if there are no pending keystrokes
|
||||||
|
if !first_keystroke && !self.pending_views.contains_key(&view_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there is a previous view context, invalidate that view if it
|
||||||
|
// has changed
|
||||||
|
if let Some(previous_view_context) = self.pending_views.remove(&view_id) {
|
||||||
|
if previous_view_context != context {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the bindings which map the pending keystrokes and current context
|
||||||
|
for binding in self.keymap.bindings().iter().rev() {
|
||||||
|
match binding.match_keys_and_context(&self.pending_keystrokes, &context) {
|
||||||
|
BindingMatchResult::Complete(mut action) => {
|
||||||
|
// Swap in keystroke for special KeyPressed action
|
||||||
|
if action.name() == "KeyPressed" && action.namespace() == "gpui" {
|
||||||
|
action = Box::new(KeyPressed {
|
||||||
|
keystroke: keystroke.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
matched_bindings.push((view_id, action))
|
||||||
|
}
|
||||||
|
BindingMatchResult::Partial => {
|
||||||
|
self.pending_views.insert(view_id, context.clone());
|
||||||
|
any_pending = true;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !any_pending {
|
||||||
|
self.clear_pending();
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matched_bindings.is_empty() {
|
||||||
|
MatchResult::Matches(matched_bindings)
|
||||||
|
} else if any_pending {
|
||||||
|
MatchResult::Pending
|
||||||
|
} else {
|
||||||
|
MatchResult::None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keystrokes_for_action(
|
||||||
|
&self,
|
||||||
|
action: &dyn Action,
|
||||||
|
context: &KeymapContext,
|
||||||
|
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||||
|
self.keymap
|
||||||
|
.bindings()
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find_map(|binding| binding.keystrokes_for_action(action, context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeymapMatcher {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(Keymap::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum MatchResult {
|
||||||
|
None,
|
||||||
|
Pending,
|
||||||
|
Matches(Vec<(usize, Box<dyn Action>)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for MatchResult {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
MatchResult::None => f.debug_struct("MatchResult::None").finish(),
|
||||||
|
MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(),
|
||||||
|
MatchResult::Matches(matches) => f
|
||||||
|
.debug_list()
|
||||||
|
.entries(
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.map(|(view_id, action)| format!("{view_id}, {}", action.name())),
|
||||||
|
)
|
||||||
|
.finish(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for MatchResult {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
(MatchResult::None, MatchResult::None) => true,
|
||||||
|
(MatchResult::Pending, MatchResult::Pending) => true,
|
||||||
|
(MatchResult::Matches(matches), MatchResult::Matches(other_matches)) => {
|
||||||
|
matches.len() == other_matches.len()
|
||||||
|
&& matches.iter().zip(other_matches.iter()).all(
|
||||||
|
|((view_id, action), (other_view_id, other_action))| {
|
||||||
|
view_id == other_view_id && action.eq(other_action.as_ref())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for MatchResult {}
|
||||||
|
|
||||||
|
impl Clone for MatchResult {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
MatchResult::None => MatchResult::None,
|
||||||
|
MatchResult::Pending => MatchResult::Pending,
|
||||||
|
MatchResult::Matches(matches) => MatchResult::Matches(
|
||||||
|
matches
|
||||||
|
.iter()
|
||||||
|
.map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::{actions, impl_actions, keymap_matcher::KeymapContext};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_push_keystroke() -> Result<()> {
|
||||||
|
actions!(test, [B, AB, C, D, DA]);
|
||||||
|
|
||||||
|
let mut context1 = KeymapContext::default();
|
||||||
|
context1.set.insert("1".into());
|
||||||
|
|
||||||
|
let mut context2 = KeymapContext::default();
|
||||||
|
context2.set.insert("2".into());
|
||||||
|
|
||||||
|
let dispatch_path = vec![(2, context2), (1, context1)];
|
||||||
|
|
||||||
|
let keymap = Keymap::new(vec![
|
||||||
|
Binding::new("a b", AB, Some("1")),
|
||||||
|
Binding::new("b", B, Some("2")),
|
||||||
|
Binding::new("c", C, Some("2")),
|
||||||
|
Binding::new("d", D, Some("1")),
|
||||||
|
Binding::new("d", D, Some("2")),
|
||||||
|
Binding::new("d a", DA, Some("2")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut matcher = KeymapMatcher::new(keymap);
|
||||||
|
|
||||||
|
// Binding with pending prefix always takes precedence
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
||||||
|
MatchResult::Pending,
|
||||||
|
);
|
||||||
|
// B alone doesn't match because a was pending, so AB is returned instead
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
|
||||||
|
MatchResult::Matches(vec![(1, Box::new(AB))]),
|
||||||
|
);
|
||||||
|
assert!(!matcher.has_pending_keystrokes());
|
||||||
|
|
||||||
|
// Without an a prefix, B is dispatched like expected
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("b")?, dispatch_path.clone()),
|
||||||
|
MatchResult::Matches(vec![(2, Box::new(B))]),
|
||||||
|
);
|
||||||
|
assert!(!matcher.has_pending_keystrokes());
|
||||||
|
|
||||||
|
// If a is prefixed, C will not be dispatched because there
|
||||||
|
// was a pending binding for it
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
||||||
|
MatchResult::Pending,
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("c")?, dispatch_path.clone()),
|
||||||
|
MatchResult::None,
|
||||||
|
);
|
||||||
|
assert!(!matcher.has_pending_keystrokes());
|
||||||
|
|
||||||
|
// If a single keystroke matches multiple bindings in the tree
|
||||||
|
// all of them are returned so that we can fallback if the action
|
||||||
|
// handler decides to propagate the action
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("d")?, dispatch_path.clone()),
|
||||||
|
MatchResult::Matches(vec![(2, Box::new(D)), (1, Box::new(D))]),
|
||||||
|
);
|
||||||
|
// If none of the d action handlers consume the binding, a pending
|
||||||
|
// binding may then be used
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("a")?, dispatch_path.clone()),
|
||||||
|
MatchResult::Matches(vec![(2, Box::new(DA))]),
|
||||||
|
);
|
||||||
|
assert!(!matcher.has_pending_keystrokes());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keystroke_parsing() -> Result<()> {
|
||||||
|
assert_eq!(
|
||||||
|
Keystroke::parse("ctrl-p")?,
|
||||||
|
Keystroke {
|
||||||
|
key: "p".into(),
|
||||||
|
ctrl: true,
|
||||||
|
alt: false,
|
||||||
|
shift: false,
|
||||||
|
cmd: false,
|
||||||
|
function: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Keystroke::parse("alt-shift-down")?,
|
||||||
|
Keystroke {
|
||||||
|
key: "down".into(),
|
||||||
|
ctrl: false,
|
||||||
|
alt: true,
|
||||||
|
shift: true,
|
||||||
|
cmd: false,
|
||||||
|
function: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Keystroke::parse("shift-cmd--")?,
|
||||||
|
Keystroke {
|
||||||
|
key: "-".into(),
|
||||||
|
ctrl: false,
|
||||||
|
alt: false,
|
||||||
|
shift: true,
|
||||||
|
cmd: true,
|
||||||
|
function: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_context_predicate_parsing() -> Result<()> {
|
||||||
|
use KeymapContextPredicate::*;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
KeymapContextPredicate::parse("a && (b == c || d != e)")?,
|
||||||
|
And(
|
||||||
|
Box::new(Identifier("a".into())),
|
||||||
|
Box::new(Or(
|
||||||
|
Box::new(Equal("b".into(), "c".into())),
|
||||||
|
Box::new(NotEqual("d".into(), "e".into())),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
KeymapContextPredicate::parse("!a")?,
|
||||||
|
Not(Box::new(Identifier("a".into())),)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_context_predicate_eval() -> Result<()> {
|
||||||
|
let predicate = KeymapContextPredicate::parse("a && b || c == d")?;
|
||||||
|
|
||||||
|
let mut context = KeymapContext::default();
|
||||||
|
context.set.insert("a".into());
|
||||||
|
assert!(!predicate.eval(&context));
|
||||||
|
|
||||||
|
context.set.insert("b".into());
|
||||||
|
assert!(predicate.eval(&context));
|
||||||
|
|
||||||
|
context.set.remove("b");
|
||||||
|
context.map.insert("c".into(), "x".into());
|
||||||
|
assert!(!predicate.eval(&context));
|
||||||
|
|
||||||
|
context.map.insert("c".into(), "d".into());
|
||||||
|
assert!(predicate.eval(&context));
|
||||||
|
|
||||||
|
let predicate = KeymapContextPredicate::parse("!a")?;
|
||||||
|
assert!(predicate.eval(&KeymapContext::default()));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_matcher() -> Result<()> {
|
||||||
|
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
|
||||||
|
pub struct A(pub String);
|
||||||
|
impl_actions!(test, [A]);
|
||||||
|
actions!(test, [B, Ab]);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
struct ActionArg {
|
||||||
|
a: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
let keymap = Keymap::new(vec![
|
||||||
|
Binding::new("a", A("x".to_string()), Some("a")),
|
||||||
|
Binding::new("b", B, Some("a")),
|
||||||
|
Binding::new("a b", Ab, Some("a || b")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut context_a = KeymapContext::default();
|
||||||
|
context_a.set.insert("a".into());
|
||||||
|
|
||||||
|
let mut context_b = KeymapContext::default();
|
||||||
|
context_b.set.insert("b".into());
|
||||||
|
|
||||||
|
let mut matcher = KeymapMatcher::new(keymap);
|
||||||
|
|
||||||
|
// Basic match
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
|
||||||
|
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
|
||||||
|
);
|
||||||
|
matcher.clear_pending();
|
||||||
|
|
||||||
|
// Multi-keystroke match
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
|
||||||
|
MatchResult::Pending
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
|
||||||
|
MatchResult::Matches(vec![(1, Box::new(Ab))])
|
||||||
|
);
|
||||||
|
matcher.clear_pending();
|
||||||
|
|
||||||
|
// Failed matches don't interfere with matching subsequent keys
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("x")?, vec![(1, context_a.clone())]),
|
||||||
|
MatchResult::None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_a.clone())]),
|
||||||
|
MatchResult::Matches(vec![(1, Box::new(A("x".to_string())))])
|
||||||
|
);
|
||||||
|
matcher.clear_pending();
|
||||||
|
|
||||||
|
// Pending keystrokes are cleared when the context changes
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("a")?, vec![(1, context_b.clone())]),
|
||||||
|
MatchResult::Pending
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_a.clone())]),
|
||||||
|
MatchResult::None
|
||||||
|
);
|
||||||
|
matcher.clear_pending();
|
||||||
|
|
||||||
|
let mut context_c = KeymapContext::default();
|
||||||
|
context_c.set.insert("c".into());
|
||||||
|
|
||||||
|
// Pending keystrokes are maintained per-view
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(
|
||||||
|
Keystroke::parse("a")?,
|
||||||
|
vec![(1, context_b.clone()), (2, context_c.clone())]
|
||||||
|
),
|
||||||
|
MatchResult::Pending
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
matcher.push_keystroke(Keystroke::parse("b")?, vec![(1, context_b.clone())]),
|
||||||
|
MatchResult::Matches(vec![(1, Box::new(Ab))])
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
104
crates/gpui/src/keymap_matcher/binding.rs
Normal file
104
crates/gpui/src/keymap_matcher/binding.rs
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::Action;
|
||||||
|
|
||||||
|
use super::{KeymapContext, KeymapContextPredicate, Keystroke};
|
||||||
|
|
||||||
|
pub struct Binding {
|
||||||
|
action: Box<dyn Action>,
|
||||||
|
keystrokes: Option<SmallVec<[Keystroke; 2]>>,
|
||||||
|
context_predicate: Option<KeymapContextPredicate>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Binding {
|
||||||
|
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||||
|
Self::load(keystrokes, Box::new(action), context).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(keystrokes: &str, action: Box<dyn Action>, context: Option<&str>) -> Result<Self> {
|
||||||
|
let context = if let Some(context) = context {
|
||||||
|
Some(KeymapContextPredicate::parse(context)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let keystrokes = if keystrokes == "*" {
|
||||||
|
None // Catch all context
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
keystrokes
|
||||||
|
.split_whitespace()
|
||||||
|
.map(Keystroke::parse)
|
||||||
|
.collect::<Result<_>>()?,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
keystrokes,
|
||||||
|
action,
|
||||||
|
context_predicate: context,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_context(&self, context: &KeymapContext) -> bool {
|
||||||
|
self.context_predicate
|
||||||
|
.as_ref()
|
||||||
|
.map(|predicate| predicate.eval(context))
|
||||||
|
.unwrap_or(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn match_keys_and_context(
|
||||||
|
&self,
|
||||||
|
pending_keystrokes: &Vec<Keystroke>,
|
||||||
|
context: &KeymapContext,
|
||||||
|
) -> BindingMatchResult {
|
||||||
|
if self
|
||||||
|
.keystrokes
|
||||||
|
.as_ref()
|
||||||
|
.map(|keystrokes| keystrokes.starts_with(&pending_keystrokes))
|
||||||
|
.unwrap_or(true)
|
||||||
|
&& self.match_context(context)
|
||||||
|
{
|
||||||
|
// If the binding is completed, push it onto the matches list
|
||||||
|
if self
|
||||||
|
.keystrokes
|
||||||
|
.as_ref()
|
||||||
|
.map(|keystrokes| keystrokes.len() == pending_keystrokes.len())
|
||||||
|
.unwrap_or(true)
|
||||||
|
{
|
||||||
|
BindingMatchResult::Complete(self.action.boxed_clone())
|
||||||
|
} else {
|
||||||
|
BindingMatchResult::Partial
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
BindingMatchResult::Fail
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keystrokes_for_action(
|
||||||
|
&self,
|
||||||
|
action: &dyn Action,
|
||||||
|
context: &KeymapContext,
|
||||||
|
) -> Option<SmallVec<[Keystroke; 2]>> {
|
||||||
|
if self.action.eq(action) && self.match_context(context) {
|
||||||
|
self.keystrokes.clone()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keystrokes(&self) -> Option<&[Keystroke]> {
|
||||||
|
self.keystrokes.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn action(&self) -> &dyn Action {
|
||||||
|
self.action.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum BindingMatchResult {
|
||||||
|
Complete(Box<dyn Action>),
|
||||||
|
Partial,
|
||||||
|
Fail,
|
||||||
|
}
|
61
crates/gpui/src/keymap_matcher/keymap.rs
Normal file
61
crates/gpui/src/keymap_matcher/keymap.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
collections::HashMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Binding;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Keymap {
|
||||||
|
bindings: Vec<Binding>,
|
||||||
|
binding_indices_by_action_type: HashMap<TypeId, SmallVec<[usize; 3]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keymap {
|
||||||
|
pub fn new(bindings: Vec<Binding>) -> Self {
|
||||||
|
let mut binding_indices_by_action_type = HashMap::new();
|
||||||
|
for (ix, binding) in bindings.iter().enumerate() {
|
||||||
|
binding_indices_by_action_type
|
||||||
|
.entry(binding.action().type_id())
|
||||||
|
.or_insert_with(SmallVec::new)
|
||||||
|
.push(ix);
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
binding_indices_by_action_type,
|
||||||
|
bindings,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn bindings_for_action_type(
|
||||||
|
&self,
|
||||||
|
action_type: TypeId,
|
||||||
|
) -> impl Iterator<Item = &'_ Binding> {
|
||||||
|
self.binding_indices_by_action_type
|
||||||
|
.get(&action_type)
|
||||||
|
.map(SmallVec::as_slice)
|
||||||
|
.unwrap_or(&[])
|
||||||
|
.iter()
|
||||||
|
.map(|ix| &self.bindings[*ix])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_bindings<T: IntoIterator<Item = Binding>>(&mut self, bindings: T) {
|
||||||
|
for binding in bindings {
|
||||||
|
self.binding_indices_by_action_type
|
||||||
|
.entry(binding.action().type_id())
|
||||||
|
.or_default()
|
||||||
|
.push(self.bindings.len());
|
||||||
|
self.bindings.push(binding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear(&mut self) {
|
||||||
|
self.bindings.clear();
|
||||||
|
self.binding_indices_by_action_type.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bindings(&self) -> &Vec<Binding> {
|
||||||
|
&self.bindings
|
||||||
|
}
|
||||||
|
}
|
123
crates/gpui/src/keymap_matcher/keymap_context.rs
Normal file
123
crates/gpui/src/keymap_matcher/keymap_context.rs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
use collections::{HashMap, HashSet};
|
||||||
|
use tree_sitter::{Language, Node, Parser};
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn tree_sitter_context_predicate() -> Language;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct KeymapContext {
|
||||||
|
pub set: HashSet<String>,
|
||||||
|
pub map: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeymapContext {
|
||||||
|
pub fn extend(&mut self, other: &Self) {
|
||||||
|
for v in &other.set {
|
||||||
|
self.set.insert(v.clone());
|
||||||
|
}
|
||||||
|
for (k, v) in &other.map {
|
||||||
|
self.map.insert(k.clone(), v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq)]
|
||||||
|
pub enum KeymapContextPredicate {
|
||||||
|
Identifier(String),
|
||||||
|
Equal(String, String),
|
||||||
|
NotEqual(String, String),
|
||||||
|
Not(Box<KeymapContextPredicate>),
|
||||||
|
And(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
|
||||||
|
Or(Box<KeymapContextPredicate>, Box<KeymapContextPredicate>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeymapContextPredicate {
|
||||||
|
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
||||||
|
let mut parser = Parser::new();
|
||||||
|
let language = unsafe { tree_sitter_context_predicate() };
|
||||||
|
parser.set_language(language).unwrap();
|
||||||
|
let source = source.as_bytes();
|
||||||
|
let tree = parser.parse(source, None).unwrap();
|
||||||
|
Self::from_node(tree.root_node(), source)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_node(node: Node, source: &[u8]) -> anyhow::Result<Self> {
|
||||||
|
let parse_error = "error parsing context predicate";
|
||||||
|
let kind = node.kind();
|
||||||
|
|
||||||
|
match kind {
|
||||||
|
"source" => Self::from_node(node.child(0).ok_or_else(|| anyhow!(parse_error))?, source),
|
||||||
|
"identifier" => Ok(Self::Identifier(node.utf8_text(source)?.into())),
|
||||||
|
"not" => {
|
||||||
|
let child = Self::from_node(
|
||||||
|
node.child_by_field_name("expression")
|
||||||
|
.ok_or_else(|| anyhow!(parse_error))?,
|
||||||
|
source,
|
||||||
|
)?;
|
||||||
|
Ok(Self::Not(Box::new(child)))
|
||||||
|
}
|
||||||
|
"and" | "or" => {
|
||||||
|
let left = Box::new(Self::from_node(
|
||||||
|
node.child_by_field_name("left")
|
||||||
|
.ok_or_else(|| anyhow!(parse_error))?,
|
||||||
|
source,
|
||||||
|
)?);
|
||||||
|
let right = Box::new(Self::from_node(
|
||||||
|
node.child_by_field_name("right")
|
||||||
|
.ok_or_else(|| anyhow!(parse_error))?,
|
||||||
|
source,
|
||||||
|
)?);
|
||||||
|
if kind == "and" {
|
||||||
|
Ok(Self::And(left, right))
|
||||||
|
} else {
|
||||||
|
Ok(Self::Or(left, right))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"equal" | "not_equal" => {
|
||||||
|
let left = node
|
||||||
|
.child_by_field_name("left")
|
||||||
|
.ok_or_else(|| anyhow!(parse_error))?
|
||||||
|
.utf8_text(source)?
|
||||||
|
.into();
|
||||||
|
let right = node
|
||||||
|
.child_by_field_name("right")
|
||||||
|
.ok_or_else(|| anyhow!(parse_error))?
|
||||||
|
.utf8_text(source)?
|
||||||
|
.into();
|
||||||
|
if kind == "equal" {
|
||||||
|
Ok(Self::Equal(left, right))
|
||||||
|
} else {
|
||||||
|
Ok(Self::NotEqual(left, right))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"parenthesized" => Self::from_node(
|
||||||
|
node.child_by_field_name("expression")
|
||||||
|
.ok_or_else(|| anyhow!(parse_error))?,
|
||||||
|
source,
|
||||||
|
),
|
||||||
|
_ => Err(anyhow!(parse_error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval(&self, context: &KeymapContext) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Identifier(name) => context.set.contains(name.as_str()),
|
||||||
|
Self::Equal(left, right) => context
|
||||||
|
.map
|
||||||
|
.get(left)
|
||||||
|
.map(|value| value == right)
|
||||||
|
.unwrap_or(false),
|
||||||
|
Self::NotEqual(left, right) => context
|
||||||
|
.map
|
||||||
|
.get(left)
|
||||||
|
.map(|value| value != right)
|
||||||
|
.unwrap_or(true),
|
||||||
|
Self::Not(pred) => !pred.eval(context),
|
||||||
|
Self::And(left, right) => left.eval(context) && right.eval(context),
|
||||||
|
Self::Or(left, right) => left.eval(context) || right.eval(context),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
97
crates/gpui/src/keymap_matcher/keystroke.rs
Normal file
97
crates/gpui/src/keymap_matcher/keystroke.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize)]
|
||||||
|
pub struct Keystroke {
|
||||||
|
pub ctrl: bool,
|
||||||
|
pub alt: bool,
|
||||||
|
pub shift: bool,
|
||||||
|
pub cmd: bool,
|
||||||
|
pub function: bool,
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keystroke {
|
||||||
|
pub fn parse(source: &str) -> anyhow::Result<Self> {
|
||||||
|
let mut ctrl = false;
|
||||||
|
let mut alt = false;
|
||||||
|
let mut shift = false;
|
||||||
|
let mut cmd = false;
|
||||||
|
let mut function = false;
|
||||||
|
let mut key = None;
|
||||||
|
|
||||||
|
let mut components = source.split('-').peekable();
|
||||||
|
while let Some(component) = components.next() {
|
||||||
|
match component {
|
||||||
|
"ctrl" => ctrl = true,
|
||||||
|
"alt" => alt = true,
|
||||||
|
"shift" => shift = true,
|
||||||
|
"cmd" => cmd = true,
|
||||||
|
"fn" => function = true,
|
||||||
|
_ => {
|
||||||
|
if let Some(component) = components.peek() {
|
||||||
|
if component.is_empty() && source.ends_with('-') {
|
||||||
|
key = Some(String::from("-"));
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("Invalid keystroke `{}`", source));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
key = Some(String::from(component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
||||||
|
|
||||||
|
Ok(Keystroke {
|
||||||
|
ctrl,
|
||||||
|
alt,
|
||||||
|
shift,
|
||||||
|
cmd,
|
||||||
|
function,
|
||||||
|
key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn modified(&self) -> bool {
|
||||||
|
self.ctrl || self.alt || self.shift || self.cmd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Keystroke {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if self.ctrl {
|
||||||
|
f.write_char('^')?;
|
||||||
|
}
|
||||||
|
if self.alt {
|
||||||
|
f.write_char('⎇')?;
|
||||||
|
}
|
||||||
|
if self.cmd {
|
||||||
|
f.write_char('⌘')?;
|
||||||
|
}
|
||||||
|
if self.shift {
|
||||||
|
f.write_char('⇧')?;
|
||||||
|
}
|
||||||
|
let key = match self.key.as_str() {
|
||||||
|
"backspace" => '⌫',
|
||||||
|
"up" => '↑',
|
||||||
|
"down" => '↓',
|
||||||
|
"left" => '←',
|
||||||
|
"right" => '→',
|
||||||
|
"tab" => '⇥',
|
||||||
|
"escape" => '⎋',
|
||||||
|
key => {
|
||||||
|
if key.len() == 1 {
|
||||||
|
key.chars().next().unwrap().to_ascii_uppercase()
|
||||||
|
} else {
|
||||||
|
return f.write_str(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
f.write_char(key)
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
rect::{RectF, RectI},
|
rect::{RectF, RectI},
|
||||||
vector::Vector2F,
|
vector::Vector2F,
|
||||||
},
|
},
|
||||||
keymap,
|
keymap_matcher::KeymapMatcher,
|
||||||
text_layout::{LineLayout, RunStyle},
|
text_layout::{LineLayout, RunStyle},
|
||||||
Action, ClipboardItem, Menu, Scene,
|
Action, ClipboardItem, Menu, Scene,
|
||||||
};
|
};
|
||||||
|
@ -87,7 +87,7 @@ pub(crate) trait ForegroundPlatform {
|
||||||
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
fn on_menu_command(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
fn on_validate_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
|
fn on_will_open_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
fn set_menus(&self, menus: Vec<Menu>, matcher: &keymap::Matcher);
|
fn set_menus(&self, menus: Vec<Menu>, matcher: &KeymapMatcher);
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
options: PathPromptOptions,
|
options: PathPromptOptions,
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::ops::Deref;
|
||||||
|
|
||||||
use pathfinder_geometry::vector::vec2f;
|
use pathfinder_geometry::vector::vec2f;
|
||||||
|
|
||||||
use crate::{geometry::vector::Vector2F, keymap::Keystroke};
|
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KeyDownEvent {
|
pub struct KeyDownEvent {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
geometry::vector::vec2f,
|
geometry::vector::vec2f,
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
platform::{Event, NavigationDirection},
|
platform::{Event, NavigationDirection},
|
||||||
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
KeyDownEvent, KeyUpEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
|
||||||
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
MouseMovedEvent, ScrollDelta, ScrollWheelEvent, TouchPhase,
|
||||||
|
@ -47,6 +47,8 @@ pub fn key_to_native(key: &str) -> Cow<str> {
|
||||||
"right" => NSRightArrowFunctionKey,
|
"right" => NSRightArrowFunctionKey,
|
||||||
"pageup" => NSPageUpFunctionKey,
|
"pageup" => NSPageUpFunctionKey,
|
||||||
"pagedown" => NSPageDownFunctionKey,
|
"pagedown" => NSPageDownFunctionKey,
|
||||||
|
"home" => NSHomeFunctionKey,
|
||||||
|
"end" => NSEndFunctionKey,
|
||||||
"delete" => NSDeleteFunctionKey,
|
"delete" => NSDeleteFunctionKey,
|
||||||
"f1" => NSF1FunctionKey,
|
"f1" => NSF1FunctionKey,
|
||||||
"f2" => NSF2FunctionKey,
|
"f2" => NSF2FunctionKey,
|
||||||
|
@ -258,6 +260,8 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
|
||||||
Some(NSRightArrowFunctionKey) => "right".to_string(),
|
Some(NSRightArrowFunctionKey) => "right".to_string(),
|
||||||
Some(NSPageUpFunctionKey) => "pageup".to_string(),
|
Some(NSPageUpFunctionKey) => "pageup".to_string(),
|
||||||
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
|
Some(NSPageDownFunctionKey) => "pagedown".to_string(),
|
||||||
|
Some(NSHomeFunctionKey) => "home".to_string(),
|
||||||
|
Some(NSEndFunctionKey) => "end".to_string(),
|
||||||
Some(NSDeleteFunctionKey) => "delete".to_string(),
|
Some(NSDeleteFunctionKey) => "delete".to_string(),
|
||||||
Some(NSF1FunctionKey) => "f1".to_string(),
|
Some(NSF1FunctionKey) => "f1".to_string(),
|
||||||
Some(NSF2FunctionKey) => "f2".to_string(),
|
Some(NSF2FunctionKey) => "f2".to_string(),
|
||||||
|
|
|
@ -3,7 +3,8 @@ use super::{
|
||||||
FontSystem, Window,
|
FontSystem, Window,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
executor, keymap,
|
executor,
|
||||||
|
keymap_matcher::KeymapMatcher,
|
||||||
platform::{self, CursorStyle},
|
platform::{self, CursorStyle},
|
||||||
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
Action, AppVersion, ClipboardItem, Event, Menu, MenuItem,
|
||||||
};
|
};
|
||||||
|
@ -135,7 +136,7 @@ impl MacForegroundPlatform {
|
||||||
menus: Vec<Menu>,
|
menus: Vec<Menu>,
|
||||||
delegate: id,
|
delegate: id,
|
||||||
actions: &mut Vec<Box<dyn Action>>,
|
actions: &mut Vec<Box<dyn Action>>,
|
||||||
keystroke_matcher: &keymap::Matcher,
|
keystroke_matcher: &KeymapMatcher,
|
||||||
) -> id {
|
) -> id {
|
||||||
let application_menu = NSMenu::new(nil).autorelease();
|
let application_menu = NSMenu::new(nil).autorelease();
|
||||||
application_menu.setDelegate_(delegate);
|
application_menu.setDelegate_(delegate);
|
||||||
|
@ -172,7 +173,7 @@ impl MacForegroundPlatform {
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
delegate: id,
|
delegate: id,
|
||||||
actions: &mut Vec<Box<dyn Action>>,
|
actions: &mut Vec<Box<dyn Action>>,
|
||||||
keystroke_matcher: &keymap::Matcher,
|
keystroke_matcher: &KeymapMatcher,
|
||||||
) -> id {
|
) -> id {
|
||||||
match item {
|
match item {
|
||||||
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
MenuItem::Separator => NSMenuItem::separatorItem(nil),
|
||||||
|
@ -183,7 +184,7 @@ impl MacForegroundPlatform {
|
||||||
.map(|binding| binding.keystrokes());
|
.map(|binding| binding.keystrokes());
|
||||||
|
|
||||||
let item;
|
let item;
|
||||||
if let Some(keystrokes) = keystrokes {
|
if let Some(keystrokes) = keystrokes.flatten() {
|
||||||
if keystrokes.len() == 1 {
|
if keystrokes.len() == 1 {
|
||||||
let keystroke = &keystrokes[0];
|
let keystroke = &keystrokes[0];
|
||||||
let mut mask = NSEventModifierFlags::empty();
|
let mut mask = NSEventModifierFlags::empty();
|
||||||
|
@ -317,7 +318,7 @@ impl platform::ForegroundPlatform for MacForegroundPlatform {
|
||||||
self.0.borrow_mut().validate_menu_command = Some(callback);
|
self.0.borrow_mut().validate_menu_command = Some(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &keymap::Matcher) {
|
fn set_menus(&self, menus: Vec<Menu>, keystroke_matcher: &KeymapMatcher) {
|
||||||
unsafe {
|
unsafe {
|
||||||
let app: id = msg_send![APP_CLASS, sharedApplication];
|
let app: id = msg_send![APP_CLASS, sharedApplication];
|
||||||
let mut state = self.0.borrow_mut();
|
let mut state = self.0.borrow_mut();
|
||||||
|
@ -647,7 +648,7 @@ impl platform::Platform for MacPlatform {
|
||||||
attrs.set(kSecReturnAttributes as *const _, cf_true);
|
attrs.set(kSecReturnAttributes as *const _, cf_true);
|
||||||
attrs.set(kSecReturnData as *const _, cf_true);
|
attrs.set(kSecReturnData as *const _, cf_true);
|
||||||
|
|
||||||
let mut result = CFTypeRef::from(ptr::null_mut());
|
let mut result = CFTypeRef::from(ptr::null());
|
||||||
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
|
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
|
||||||
match status {
|
match status {
|
||||||
security::errSecSuccess => {}
|
security::errSecSuccess => {}
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
|
mac::platform::NSViewLayerContentsRedrawDuringViewResize,
|
||||||
platform::{
|
platform::{
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -4,7 +4,8 @@ use crate::{
|
||||||
rect::RectF,
|
rect::RectF,
|
||||||
vector::{vec2f, Vector2F},
|
vector::{vec2f, Vector2F},
|
||||||
},
|
},
|
||||||
keymap, Action, ClipboardItem,
|
keymap_matcher::KeymapMatcher,
|
||||||
|
Action, ClipboardItem,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
@ -84,7 +85,7 @@ impl super::ForegroundPlatform for ForegroundPlatform {
|
||||||
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
|
fn on_menu_command(&self, _: Box<dyn FnMut(&dyn Action)>) {}
|
||||||
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
|
fn on_validate_menu_command(&self, _: Box<dyn FnMut(&dyn Action) -> bool>) {}
|
||||||
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
|
fn on_will_open_menu(&self, _: Box<dyn FnMut()>) {}
|
||||||
fn set_menus(&self, _: Vec<crate::Menu>, _: &keymap::Matcher) {}
|
fn set_menus(&self, _: Vec<crate::Menu>, _: &KeymapMatcher) {}
|
||||||
|
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
font_cache::FontCache,
|
font_cache::FontCache,
|
||||||
geometry::rect::RectF,
|
geometry::rect::RectF,
|
||||||
json::{self, ToJson},
|
json::{self, ToJson},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
platform::{CursorStyle, Event},
|
platform::{CursorStyle, Event},
|
||||||
scene::{
|
scene::{
|
||||||
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
|
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
|
||||||
|
|
|
@ -162,12 +162,10 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
|
||||||
if let FnArg::Typed(arg) = arg {
|
if let FnArg::Typed(arg) = arg {
|
||||||
if let Type::Path(ty) = &*arg.ty {
|
if let Type::Path(ty) = &*arg.ty {
|
||||||
let last_segment = ty.path.segments.last();
|
let last_segment = ty.path.segments.last();
|
||||||
match last_segment.map(|s| s.ident.to_string()).as_deref() {
|
|
||||||
Some("StdRng") => {
|
if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
|
||||||
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
|
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(seed),));
|
||||||
}
|
}
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
inner_fn_args.extend(quote!(cx,));
|
inner_fn_args.extend(quote!(cx,));
|
||||||
}
|
}
|
||||||
|
|
|
@ -682,7 +682,6 @@ impl Buffer {
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
pub fn diff_base(&self) -> Option<&str> {
|
pub fn diff_base(&self) -> Option<&str> {
|
||||||
self.diff_base.as_deref()
|
self.diff_base.as_deref()
|
||||||
}
|
}
|
||||||
|
|
|
@ -289,6 +289,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
|
||||||
);
|
);
|
||||||
|
|
||||||
buffer.update(cx, |buf, cx| {
|
buffer.update(cx, |buf, cx| {
|
||||||
|
buf.undo(cx);
|
||||||
|
buf.undo(cx);
|
||||||
|
buf.undo(cx);
|
||||||
buf.undo(cx);
|
buf.undo(cx);
|
||||||
assert_eq!(buf.text(), "fn a() {}");
|
assert_eq!(buf.text(), "fn a() {}");
|
||||||
assert!(buf.is_parsing());
|
assert!(buf.is_parsing());
|
||||||
|
@ -304,6 +307,9 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
|
||||||
);
|
);
|
||||||
|
|
||||||
buffer.update(cx, |buf, cx| {
|
buffer.update(cx, |buf, cx| {
|
||||||
|
buf.redo(cx);
|
||||||
|
buf.redo(cx);
|
||||||
|
buf.redo(cx);
|
||||||
buf.redo(cx);
|
buf.redo(cx);
|
||||||
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
|
assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
|
||||||
assert!(buf.is_parsing());
|
assert!(buf.is_parsing());
|
||||||
|
@ -1022,8 +1028,11 @@ fn test_autoindent_block_mode(cx: &mut MutableAppContext) {
|
||||||
.unindent()
|
.unindent()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Grouping is disabled in tests, so we need 2 undos
|
||||||
|
buffer.undo(cx); // Undo the auto-indent
|
||||||
|
buffer.undo(cx); // Undo the original edit
|
||||||
|
|
||||||
// Insert the block at a deeper indent level. The entire block is outdented.
|
// Insert the block at a deeper indent level. The entire block is outdented.
|
||||||
buffer.undo(cx);
|
|
||||||
buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
|
buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
|
||||||
buffer.edit(
|
buffer.edit(
|
||||||
[(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
|
[(Point::new(2, 8)..Point::new(2, 8), inserted_text)],
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::{
|
||||||
process::Command,
|
process::Command,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SWIFT_PACKAGE_NAME: &'static str = "LiveKitBridge";
|
const SWIFT_PACKAGE_NAME: &str = "LiveKitBridge";
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
|
@ -61,8 +61,8 @@ fn build_bridge(swift_target: &SwiftTarget) {
|
||||||
let swift_package_root = swift_package_root();
|
let swift_package_root = swift_package_root();
|
||||||
if !Command::new("swift")
|
if !Command::new("swift")
|
||||||
.arg("build")
|
.arg("build")
|
||||||
.args(&["--configuration", &env::var("PROFILE").unwrap()])
|
.args(["--configuration", &env::var("PROFILE").unwrap()])
|
||||||
.args(&["--triple", &swift_target.target.triple])
|
.args(["--triple", &swift_target.target.triple])
|
||||||
.current_dir(&swift_package_root)
|
.current_dir(&swift_package_root)
|
||||||
.status()
|
.status()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -116,7 +116,7 @@ fn get_swift_target() -> SwiftTarget {
|
||||||
let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
|
let target = format!("{}-apple-macosx{}", arch, MACOS_TARGET_VERSION);
|
||||||
|
|
||||||
let swift_target_info_str = Command::new("swift")
|
let swift_target_info_str = Command::new("swift")
|
||||||
.args(&["-target", &target, "-print-target-info"])
|
.args(["-target", &target, "-print-target-info"])
|
||||||
.output()
|
.output()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.stdout;
|
.stdout;
|
||||||
|
@ -143,7 +143,7 @@ fn copy_dir(source: &Path, destination: &Path) {
|
||||||
assert!(
|
assert!(
|
||||||
Command::new("cp")
|
Command::new("cp")
|
||||||
.arg("-R")
|
.arg("-R")
|
||||||
.args(&[source, destination])
|
.args([source, destination])
|
||||||
.status()
|
.status()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.success(),
|
.success(),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{actions, keymap::Binding, Menu, MenuItem};
|
use gpui::{actions, keymap_matcher::Binding, Menu, MenuItem};
|
||||||
use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
|
use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room};
|
||||||
use live_kit_server::token::{self, VideoGrant};
|
use live_kit_server::token::{self, VideoGrant};
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::{env, path::PathBuf, process::Command};
|
||||||
fn main() {
|
fn main() {
|
||||||
let sdk_path = String::from_utf8(
|
let sdk_path = String::from_utf8(
|
||||||
Command::new("xcrun")
|
Command::new("xcrun")
|
||||||
.args(&["--sdk", "macosx", "--show-sdk-path"])
|
.args(["--sdk", "macosx", "--show-sdk-path"])
|
||||||
.output()
|
.output()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.stdout,
|
.stdout,
|
||||||
|
|
|
@ -113,9 +113,9 @@ pub mod core_video {
|
||||||
let mut this = ptr::null();
|
let mut this = ptr::null();
|
||||||
let result = CVMetalTextureCacheCreate(
|
let result = CVMetalTextureCacheCreate(
|
||||||
kCFAllocatorDefault,
|
kCFAllocatorDefault,
|
||||||
ptr::null_mut(),
|
ptr::null(),
|
||||||
metal_device,
|
metal_device,
|
||||||
ptr::null_mut(),
|
ptr::null(),
|
||||||
&mut this,
|
&mut this,
|
||||||
);
|
);
|
||||||
if result == kCVReturnSuccess {
|
if result == kCVReturnSuccess {
|
||||||
|
@ -192,7 +192,7 @@ pub mod core_video {
|
||||||
pub fn as_texture_ref(&self) -> &metal::TextureRef {
|
pub fn as_texture_ref(&self) -> &metal::TextureRef {
|
||||||
unsafe {
|
unsafe {
|
||||||
let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
|
let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
|
||||||
&metal::TextureRef::from_ptr(texture as *mut _)
|
metal::TextureRef::from_ptr(texture as *mut _)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*,
|
elements::*,
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
keymap,
|
keymap_matcher::KeymapContext,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
|
AnyViewHandle, AppContext, Axis, Entity, MouseButton, MouseState, MutableAppContext,
|
||||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
|
@ -124,7 +124,7 @@ impl<D: PickerDelegate> View for Picker<D> {
|
||||||
.named("picker")
|
.named("picker")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -5189,11 +5189,7 @@ impl Project {
|
||||||
|
|
||||||
let operations = buffer.serialize_ops(Some(remote_version), cx);
|
let operations = buffer.serialize_ops(Some(remote_version), cx);
|
||||||
let client = this.client.clone();
|
let client = this.client.clone();
|
||||||
let file = buffer.file().cloned();
|
if let Some(file) = buffer.file() {
|
||||||
cx.background()
|
|
||||||
.spawn(
|
|
||||||
async move {
|
|
||||||
if let Some(file) = file {
|
|
||||||
client
|
client
|
||||||
.send(proto::UpdateBufferFile {
|
.send(proto::UpdateBufferFile {
|
||||||
project_id,
|
project_id,
|
||||||
|
@ -5203,6 +5199,17 @@ impl Project {
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client
|
||||||
|
.send(proto::UpdateDiffBase {
|
||||||
|
project_id,
|
||||||
|
buffer_id: buffer_id as u64,
|
||||||
|
diff_base: buffer.diff_base().map(Into::into),
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
|
||||||
|
cx.background()
|
||||||
|
.spawn(
|
||||||
|
async move {
|
||||||
let operations = operations.await;
|
let operations = operations.await;
|
||||||
for chunk in split_operations(operations) {
|
for chunk in split_operations(operations) {
|
||||||
client
|
client
|
||||||
|
|
|
@ -10,7 +10,8 @@ use gpui::{
|
||||||
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
|
||||||
},
|
},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_internal_actions, keymap,
|
impl_internal_actions,
|
||||||
|
keymap_matcher::KeymapContext,
|
||||||
platform::CursorStyle,
|
platform::CursorStyle,
|
||||||
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
|
AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
|
||||||
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
|
||||||
|
@ -1301,7 +1302,7 @@ impl View for ProjectPanel {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut cx = Self::default_keymap_context();
|
let mut cx = Self::default_keymap_context();
|
||||||
cx.set.insert("menu".into());
|
cx.set.insert("menu".into());
|
||||||
cx
|
cx
|
||||||
|
|
|
@ -146,6 +146,7 @@ impl PickerDelegate for RecentProjectsView {
|
||||||
.matches
|
.matches
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
|
.rev()
|
||||||
.max_by_key(|(_, m)| OrderedFloat(m.score))
|
.max_by_key(|(_, m)| OrderedFloat(m.score))
|
||||||
.map(|(ix, _)| ix)
|
.map(|(ix, _)| ix)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
|
@ -23,7 +23,7 @@ pub fn random_token() -> String {
|
||||||
for byte in token_bytes.iter_mut() {
|
for byte in token_bytes.iter_mut() {
|
||||||
*byte = rng.gen();
|
*byte = rng.gen();
|
||||||
}
|
}
|
||||||
base64::encode_config(&token_bytes, base64::URL_SAFE)
|
base64::encode_config(token_bytes, base64::URL_SAFE)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PublicKey {
|
impl PublicKey {
|
||||||
|
|
|
@ -103,6 +103,8 @@ impl View for BufferSearchBar {
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
|
@ -173,6 +175,10 @@ impl View for BufferSearchBar {
|
||||||
.aligned()
|
.aligned()
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
|
.flex(1., true)
|
||||||
|
.boxed(),
|
||||||
|
)
|
||||||
|
.with_child(self.render_close_button(&theme.search, cx))
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(theme.search.container)
|
.with_style(theme.search.container)
|
||||||
.named("search bar")
|
.named("search bar")
|
||||||
|
@ -325,7 +331,7 @@ impl BufferSearchBar {
|
||||||
let is_active = self.is_search_option_enabled(option);
|
let is_active = self.is_search_option_enabled(option);
|
||||||
Some(
|
Some(
|
||||||
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
|
MouseEventHandler::<Self>::new(option as usize, cx, |state, cx| {
|
||||||
let style = &cx
|
let style = cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
.search
|
.search
|
||||||
|
@ -373,7 +379,7 @@ impl BufferSearchBar {
|
||||||
|
|
||||||
enum NavButton {}
|
enum NavButton {}
|
||||||
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
|
MouseEventHandler::<NavButton>::new(direction as usize, cx, |state, cx| {
|
||||||
let style = &cx
|
let style = cx
|
||||||
.global::<Settings>()
|
.global::<Settings>()
|
||||||
.theme
|
.theme
|
||||||
.search
|
.search
|
||||||
|
@ -399,6 +405,38 @@ impl BufferSearchBar {
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_close_button(
|
||||||
|
&self,
|
||||||
|
theme: &theme::Search,
|
||||||
|
cx: &mut RenderContext<Self>,
|
||||||
|
) -> ElementBox {
|
||||||
|
let action = Box::new(Dismiss);
|
||||||
|
let tooltip = "Dismiss Buffer Search";
|
||||||
|
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||||
|
|
||||||
|
enum CloseButton {}
|
||||||
|
MouseEventHandler::<CloseButton>::new(0, cx, |state, _| {
|
||||||
|
let style = theme.dismiss_button.style_for(state, false);
|
||||||
|
Svg::new("icons/x_mark_8.svg")
|
||||||
|
.with_color(style.color)
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.icon_width)
|
||||||
|
.aligned()
|
||||||
|
.constrained()
|
||||||
|
.with_width(style.button_width)
|
||||||
|
.contained()
|
||||||
|
.with_style(style.container)
|
||||||
|
.boxed()
|
||||||
|
})
|
||||||
|
.on_click(MouseButton::Left, {
|
||||||
|
let action = action.boxed_clone();
|
||||||
|
move |_, cx| cx.dispatch_any_action(action.boxed_clone())
|
||||||
|
})
|
||||||
|
.with_cursor_style(CursorStyle::PointingHand)
|
||||||
|
.with_tooltip::<CloseButton, _>(0, tooltip.to_string(), Some(action), tooltip_style, cx)
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
|
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
|
||||||
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
|
||||||
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
|
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{parse_json_with_comments, Settings};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use collections::BTreeMap;
|
use collections::BTreeMap;
|
||||||
use gpui::{keymap::Binding, MutableAppContext};
|
use gpui::{keymap_matcher::Binding, MutableAppContext};
|
||||||
use schemars::{
|
use schemars::{
|
||||||
gen::{SchemaGenerator, SchemaSettings},
|
gen::{SchemaGenerator, SchemaSettings},
|
||||||
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
|
||||||
|
|
|
@ -62,7 +62,7 @@ fn parse_snippet<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
let chunk_end = source.find(&['}', '$', '\\']).unwrap_or(source.len());
|
let chunk_end = source.find(['}', '$', '\\']).unwrap_or(source.len());
|
||||||
let (chunk, rest) = source.split_at(chunk_end);
|
let (chunk, rest) = source.split_at(chunk_end);
|
||||||
text.push_str(chunk);
|
text.push_str(chunk);
|
||||||
source = rest;
|
source = rest;
|
||||||
|
|
|
@ -20,7 +20,7 @@ unsafe impl Send for Connection {}
|
||||||
impl Connection {
|
impl Connection {
|
||||||
pub(crate) fn open(uri: &str, persistent: bool) -> Result<Self> {
|
pub(crate) fn open(uri: &str, persistent: bool) -> Result<Self> {
|
||||||
let mut connection = Self {
|
let mut connection = Self {
|
||||||
sqlite3: 0 as *mut _,
|
sqlite3: ptr::null_mut(),
|
||||||
persistent,
|
persistent,
|
||||||
write: RefCell::new(true),
|
write: RefCell::new(true),
|
||||||
_sqlite: PhantomData,
|
_sqlite: PhantomData,
|
||||||
|
@ -32,7 +32,7 @@ impl Connection {
|
||||||
CString::new(uri)?.as_ptr(),
|
CString::new(uri)?.as_ptr(),
|
||||||
&mut connection.sqlite3,
|
&mut connection.sqlite3,
|
||||||
flags,
|
flags,
|
||||||
0 as *const _,
|
ptr::null(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Turn on extended error codes
|
// Turn on extended error codes
|
||||||
|
@ -97,7 +97,7 @@ impl Connection {
|
||||||
let remaining_sql_str = remaining_sql.to_str().unwrap().trim();
|
let remaining_sql_str = remaining_sql.to_str().unwrap().trim();
|
||||||
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
|
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
|
||||||
} {
|
} {
|
||||||
let mut raw_statement = 0 as *mut sqlite3_stmt;
|
let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
|
||||||
let mut remaining_sql_ptr = ptr::null();
|
let mut remaining_sql_ptr = ptr::null();
|
||||||
sqlite3_prepare_v2(
|
sqlite3_prepare_v2(
|
||||||
self.sqlite3,
|
self.sqlite3,
|
||||||
|
|
|
@ -48,7 +48,7 @@ impl<'a> Statement<'a> {
|
||||||
.trim();
|
.trim();
|
||||||
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
|
remaining_sql_str != ";" && !remaining_sql_str.is_empty()
|
||||||
} {
|
} {
|
||||||
let mut raw_statement = 0 as *mut sqlite3_stmt;
|
let mut raw_statement = ptr::null_mut::<sqlite3_stmt>();
|
||||||
let mut remaining_sql_ptr = ptr::null();
|
let mut remaining_sql_ptr = ptr::null();
|
||||||
sqlite3_prepare_v2(
|
sqlite3_prepare_v2(
|
||||||
connection.sqlite3,
|
connection.sqlite3,
|
||||||
|
@ -101,7 +101,7 @@ impl<'a> Statement<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt) -> ()) -> Result<()> {
|
fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt)) -> Result<()> {
|
||||||
let mut any_succeed = false;
|
let mut any_succeed = false;
|
||||||
unsafe {
|
unsafe {
|
||||||
for raw_statement in self.raw_statements.iter() {
|
for raw_statement in self.raw_statements.iter() {
|
||||||
|
@ -133,7 +133,7 @@ impl<'a> Statement<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_blob<'b>(&'b mut self, index: i32) -> Result<&'b [u8]> {
|
pub fn column_blob(&mut self, index: i32) -> Result<&[u8]> {
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) };
|
let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) };
|
||||||
|
|
||||||
|
@ -217,7 +217,7 @@ impl<'a> Statement<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn column_text<'b>(&'b mut self, index: i32) -> Result<&'b str> {
|
pub fn column_text(&mut self, index: i32) -> Result<&str> {
|
||||||
let index = index as c_int;
|
let index = index as c_int;
|
||||||
let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) };
|
let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) };
|
||||||
|
|
||||||
|
|
|
@ -114,12 +114,12 @@ impl<M: Migrator> ThreadSafeConnection<M> {
|
||||||
let mut queues = QUEUES.write();
|
let mut queues = QUEUES.write();
|
||||||
if !queues.contains_key(&self.uri) {
|
if !queues.contains_key(&self.uri) {
|
||||||
let mut write_queue_constructor =
|
let mut write_queue_constructor =
|
||||||
write_queue_constructor.unwrap_or(background_thread_queue());
|
write_queue_constructor.unwrap_or_else(background_thread_queue);
|
||||||
queues.insert(self.uri.clone(), write_queue_constructor());
|
queues.insert(self.uri.clone(), write_queue_constructor());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder<M> {
|
pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder<M> {
|
||||||
|
@ -187,10 +187,9 @@ impl<M: Migrator> ThreadSafeConnection<M> {
|
||||||
*connection.write.get_mut() = false;
|
*connection.write.get_mut() = false;
|
||||||
|
|
||||||
if let Some(initialize_query) = connection_initialize_query {
|
if let Some(initialize_query) = connection_initialize_query {
|
||||||
connection.exec(initialize_query).expect(&format!(
|
connection.exec(initialize_query).unwrap_or_else(|_| {
|
||||||
"Initialize query failed to execute: {}",
|
panic!("Initialize query failed to execute: {}", initialize_query)
|
||||||
initialize_query
|
})()
|
||||||
))()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,7 +224,7 @@ impl<M: Migrator> Clone for ThreadSafeConnection<M> {
|
||||||
Self {
|
Self {
|
||||||
uri: self.uri.clone(),
|
uri: self.uri.clone(),
|
||||||
persistent: self.persistent,
|
persistent: self.persistent,
|
||||||
connection_initialize_query: self.connection_initialize_query.clone(),
|
connection_initialize_query: self.connection_initialize_query,
|
||||||
connections: self.connections.clone(),
|
connections: self.connections.clone(),
|
||||||
_migrator: PhantomData,
|
_migrator: PhantomData,
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
|
|
||||||
impl Connection {
|
impl Connection {
|
||||||
pub fn exec<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<()>> {
|
pub fn exec<'a>(&'a self, query: &str) -> Result<impl 'a + FnMut() -> Result<()>> {
|
||||||
let mut statement = Statement::prepare(&self, query)?;
|
let mut statement = Statement::prepare(self, query)?;
|
||||||
Ok(move || statement.exec())
|
Ok(move || statement.exec())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ impl Connection {
|
||||||
&'a self,
|
&'a self,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
|
) -> Result<impl 'a + FnMut(B) -> Result<()>> {
|
||||||
let mut statement = Statement::prepare(&self, query)?;
|
let mut statement = Statement::prepare(self, query)?;
|
||||||
Ok(move |bindings| statement.with_bindings(bindings)?.exec())
|
Ok(move |bindings| statement.with_bindings(bindings)?.exec())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ impl Connection {
|
||||||
&'a self,
|
&'a self,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<impl 'a + FnMut() -> Result<Vec<C>>> {
|
) -> Result<impl 'a + FnMut() -> Result<Vec<C>>> {
|
||||||
let mut statement = Statement::prepare(&self, query)?;
|
let mut statement = Statement::prepare(self, query)?;
|
||||||
Ok(move || statement.rows::<C>())
|
Ok(move || statement.rows::<C>())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ impl Connection {
|
||||||
&'a self,
|
&'a self,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
|
) -> Result<impl 'a + FnMut(B) -> Result<Vec<C>>> {
|
||||||
let mut statement = Statement::prepare(&self, query)?;
|
let mut statement = Statement::prepare(self, query)?;
|
||||||
Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
|
Ok(move |bindings| statement.with_bindings(bindings)?.rows::<C>())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ impl Connection {
|
||||||
&'a self,
|
&'a self,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<impl 'a + FnMut() -> Result<Option<C>>> {
|
) -> Result<impl 'a + FnMut() -> Result<Option<C>>> {
|
||||||
let mut statement = Statement::prepare(&self, query)?;
|
let mut statement = Statement::prepare(self, query)?;
|
||||||
Ok(move || statement.maybe_row::<C>())
|
Ok(move || statement.maybe_row::<C>())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,7 +48,7 @@ impl Connection {
|
||||||
&'a self,
|
&'a self,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Result<impl 'a + FnMut(B) -> Result<Option<C>>> {
|
) -> Result<impl 'a + FnMut(B) -> Result<Option<C>>> {
|
||||||
let mut statement = Statement::prepare(&self, query)?;
|
let mut statement = Statement::prepare(self, query)?;
|
||||||
Ok(move |bindings| {
|
Ok(move |bindings| {
|
||||||
statement
|
statement
|
||||||
.with_bindings(bindings)
|
.with_bindings(bindings)
|
||||||
|
|
|
@ -33,14 +33,14 @@ fn create_error(
|
||||||
.skip_while(|(offset, _)| offset <= &error_offset)
|
.skip_while(|(offset, _)| offset <= &error_offset)
|
||||||
.map(|(_, span)| span)
|
.map(|(_, span)| span)
|
||||||
.next()
|
.next()
|
||||||
.unwrap_or(Span::call_site());
|
.unwrap_or_else(Span::call_site);
|
||||||
let error_text = format!("Sql Error: {}\nFor Query: {}", error, formatted_sql);
|
let error_text = format!("Sql Error: {}\nFor Query: {}", error, formatted_sql);
|
||||||
TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error())
|
TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_sql(tokens: TokenStream) -> (Vec<(usize, Span)>, String) {
|
fn make_sql(tokens: TokenStream) -> (Vec<(usize, Span)>, String) {
|
||||||
let mut sql_tokens = vec![];
|
let mut sql_tokens = vec![];
|
||||||
flatten_stream(tokens.clone(), &mut sql_tokens);
|
flatten_stream(tokens, &mut sql_tokens);
|
||||||
// Lookup of spans by offset at the end of the token
|
// Lookup of spans by offset at the end of the token
|
||||||
let mut spans: Vec<(usize, Span)> = Vec::new();
|
let mut spans: Vec<(usize, Span)> = Vec::new();
|
||||||
let mut sql = String::new();
|
let mut sql = String::new();
|
||||||
|
@ -67,7 +67,7 @@ fn flatten_stream(tokens: TokenStream, result: &mut Vec<(String, Span)>) {
|
||||||
result.push((close_delimiter(group.delimiter()), group.span()));
|
result.push((close_delimiter(group.delimiter()), group.span()));
|
||||||
}
|
}
|
||||||
TokenTree::Ident(ident) => {
|
TokenTree::Ident(ident) => {
|
||||||
result.push((format!("{} ", ident.to_string()), ident.span()));
|
result.push((format!("{} ", ident), ident.span()));
|
||||||
}
|
}
|
||||||
leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())),
|
leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())),
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ impl<K: Clone + Debug + Default + Ord, V: Clone + Debug> TreeMap<K, V> {
|
||||||
self.0.insert_or_replace(MapEntry { key, value }, &());
|
self.0.insert_or_replace(MapEntry { key, value }, &());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove<'a>(&mut self, key: &'a K) -> Option<V> {
|
pub fn remove(&mut self, key: &K) -> Option<V> {
|
||||||
let mut removed = None;
|
let mut removed = None;
|
||||||
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
|
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>();
|
||||||
let key = MapKeyRef(Some(key));
|
let key = MapKeyRef(Some(key));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/// The mappings defined in this file where created from reading the alacritty source
|
/// The mappings defined in this file where created from reading the alacritty source
|
||||||
use alacritty_terminal::term::TermMode;
|
use alacritty_terminal::term::TermMode;
|
||||||
use gpui::keymap::Keystroke;
|
use gpui::keymap_matcher::Keystroke;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Modifiers {
|
pub enum Modifiers {
|
||||||
|
@ -273,6 +273,8 @@ fn modifier_code(keystroke: &Keystroke) -> u32 {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use gpui::keymap_matcher::Keystroke;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -50,7 +50,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
geometry::vector::{vec2f, Vector2F},
|
geometry::vector::{vec2f, Vector2F},
|
||||||
keymap::Keystroke,
|
keymap_matcher::Keystroke,
|
||||||
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
|
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
|
||||||
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task,
|
ClipboardItem, Entity, ModelContext, MouseButton, MouseMovedEvent, Task,
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,7 +14,7 @@ use gpui::{
|
||||||
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
|
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
|
||||||
geometry::vector::Vector2F,
|
geometry::vector::Vector2F,
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
keymap::Keystroke,
|
keymap_matcher::{KeymapContext, Keystroke},
|
||||||
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
|
AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, Task,
|
||||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
@ -465,7 +465,7 @@ impl View for TerminalView {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, cx: &gpui::AppContext) -> gpui::keymap::Context {
|
fn keymap_context(&self, cx: &gpui::AppContext) -> KeymapContext {
|
||||||
let mut context = Self::default_keymap_context();
|
let mut context = Self::default_keymap_context();
|
||||||
|
|
||||||
let mode = self.terminal.read(cx).last_content.mode;
|
let mode = self.terminal.read(cx).last_content.mode;
|
||||||
|
|
|
@ -247,6 +247,7 @@ pub struct Search {
|
||||||
pub results_status: TextStyle,
|
pub results_status: TextStyle,
|
||||||
pub tab_icon_width: f32,
|
pub tab_icon_width: f32,
|
||||||
pub tab_icon_spacing: f32,
|
pub tab_icon_spacing: f32,
|
||||||
|
pub dismiss_button: Interactive<IconButton>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, Default)]
|
#[derive(Clone, Deserialize, Default)]
|
||||||
|
|
|
@ -4,7 +4,7 @@ use lazy_static::lazy_static;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
|
pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL")
|
||||||
.unwrap_or(include_str!("../../zed/RELEASE_CHANNEL").to_string());
|
.unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").to_string());
|
||||||
pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
|
pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() {
|
||||||
"dev" => ReleaseChannel::Dev,
|
"dev" => ReleaseChannel::Dev,
|
||||||
"preview" => ReleaseChannel::Preview,
|
"preview" => ReleaseChannel::Preview,
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String {
|
||||||
debug_assert!(max_chars >= 5);
|
debug_assert!(max_chars >= 5);
|
||||||
|
|
||||||
if s.len() > max_chars {
|
if s.len() > max_chars {
|
||||||
format!("{}…", truncate(&s, max_chars.saturating_sub(3)))
|
format!("{}…", truncate(s, max_chars.saturating_sub(3)))
|
||||||
} else {
|
} else {
|
||||||
s.to_string()
|
s.to_string()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use editor::{
|
||||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||||
movement, Bias, CharKind, DisplayPoint,
|
movement, Bias, CharKind, DisplayPoint,
|
||||||
};
|
};
|
||||||
use gpui::{actions, impl_actions, MutableAppContext};
|
use gpui::{actions, impl_actions, keymap_matcher::KeyPressed, MutableAppContext};
|
||||||
use language::{Point, Selection, SelectionGoal};
|
use language::{Point, Selection, SelectionGoal};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
@ -32,6 +32,8 @@ pub enum Motion {
|
||||||
StartOfDocument,
|
StartOfDocument,
|
||||||
EndOfDocument,
|
EndOfDocument,
|
||||||
Matching,
|
Matching,
|
||||||
|
FindForward { before: bool, character: char },
|
||||||
|
FindBackward { after: bool, character: char },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
@ -107,10 +109,34 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
|
||||||
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
|
||||||
);
|
);
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| match Vim::read(cx)
|
||||||
|
.active_operator()
|
||||||
|
{
|
||||||
|
Some(Operator::FindForward { before }) => motion(
|
||||||
|
Motion::FindForward {
|
||||||
|
before,
|
||||||
|
character: keystroke.key.chars().next().unwrap(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
Some(Operator::FindBackward { after }) => motion(
|
||||||
|
Motion::FindBackward {
|
||||||
|
after,
|
||||||
|
character: keystroke.key.chars().next().unwrap(),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
),
|
||||||
|
_ => cx.propagate_action(),
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
|
||||||
if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
|
if let Some(Operator::Namespace(_))
|
||||||
|
| Some(Operator::FindForward { .. })
|
||||||
|
| Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
|
||||||
|
{
|
||||||
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,14 +178,16 @@ impl Motion {
|
||||||
| CurrentLine
|
| CurrentLine
|
||||||
| EndOfLine
|
| EndOfLine
|
||||||
| NextWordEnd { .. }
|
| NextWordEnd { .. }
|
||||||
| Matching => true,
|
| Matching
|
||||||
|
| FindForward { .. } => true,
|
||||||
Left
|
Left
|
||||||
| Backspace
|
| Backspace
|
||||||
| Right
|
| Right
|
||||||
| StartOfLine
|
| StartOfLine
|
||||||
| NextWordStart { .. }
|
| NextWordStart { .. }
|
||||||
| PreviousWordStart { .. }
|
| PreviousWordStart { .. }
|
||||||
| FirstNonWhitespace => false,
|
| FirstNonWhitespace
|
||||||
|
| FindBackward { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,6 +224,14 @@ impl Motion {
|
||||||
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
|
||||||
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
|
EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
|
||||||
Matching => (matching(map, point), SelectionGoal::None),
|
Matching => (matching(map, point), SelectionGoal::None),
|
||||||
|
FindForward { before, character } => (
|
||||||
|
find_forward(map, point, before, character, times),
|
||||||
|
SelectionGoal::None,
|
||||||
|
),
|
||||||
|
FindBackward { after, character } => (
|
||||||
|
find_backward(map, point, after, character, times),
|
||||||
|
SelectionGoal::None,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
(new_point != point || self.infallible()).then_some((new_point, goal))
|
(new_point != point || self.infallible()).then_some((new_point, goal))
|
||||||
|
@ -446,3 +482,50 @@ fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
|
||||||
point
|
point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_forward(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
before: bool,
|
||||||
|
target: char,
|
||||||
|
mut times: usize,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let mut previous_point = from;
|
||||||
|
|
||||||
|
for (ch, point) in map.chars_at(from) {
|
||||||
|
if ch == target && point != from {
|
||||||
|
times -= 1;
|
||||||
|
if times == 0 {
|
||||||
|
return if before { previous_point } else { point };
|
||||||
|
}
|
||||||
|
} else if ch == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
previous_point = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
from
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_backward(
|
||||||
|
map: &DisplaySnapshot,
|
||||||
|
from: DisplayPoint,
|
||||||
|
after: bool,
|
||||||
|
target: char,
|
||||||
|
mut times: usize,
|
||||||
|
) -> DisplayPoint {
|
||||||
|
let mut previous_point = from;
|
||||||
|
for (ch, point) in map.reverse_chars_at(from) {
|
||||||
|
if ch == target && point != from {
|
||||||
|
times -= 1;
|
||||||
|
if times == 0 {
|
||||||
|
return if after { previous_point } else { point };
|
||||||
|
}
|
||||||
|
} else if ch == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
previous_point = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
from
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ use editor::{
|
||||||
};
|
};
|
||||||
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
|
use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
|
||||||
use language::{AutoindentMode, Point, SelectionGoal};
|
use language::{AutoindentMode, Point, SelectionGoal};
|
||||||
|
use log::error;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
@ -101,8 +102,9 @@ pub fn normal_motion(
|
||||||
Some(Operator::Change) => change_motion(vim, motion, times, cx),
|
Some(Operator::Change) => change_motion(vim, motion, times, cx),
|
||||||
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
||||||
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
||||||
_ => {
|
Some(operator) => {
|
||||||
// Can't do anything for text objects or namespace operators. Ignoring
|
// Can't do anything for text objects or namespace operators. Ignoring
|
||||||
|
error!("Unexpected normal mode motion operator: {:?}", operator)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -912,4 +914,42 @@ mod test {
|
||||||
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
|
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["h"]);
|
||||||
cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
|
cx.assert_all("Testˇ├ˇ──ˇ┐ˇTest").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_f_and_t(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
for count in 1..=3 {
|
||||||
|
let test_case = indoc! {"
|
||||||
|
ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
|
||||||
|
ˇ ˇbˇaaˇa ˇbˇbˇb
|
||||||
|
ˇ
|
||||||
|
ˇb
|
||||||
|
"};
|
||||||
|
|
||||||
|
cx.assert_binding_matches_all([&count.to_string(), "f", "b"], test_case)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.assert_binding_matches_all([&count.to_string(), "t", "b"], test_case)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
for count in 1..=3 {
|
||||||
|
let test_case = indoc! {"
|
||||||
|
ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa
|
||||||
|
ˇ ˇbˇaaˇa ˇbˇbˇb
|
||||||
|
ˇ
|
||||||
|
ˇb
|
||||||
|
"};
|
||||||
|
|
||||||
|
cx.assert_binding_matches_all([&count.to_string(), "shift-f", "b"], test_case)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.assert_binding_matches_all([&count.to_string(), "shift-t", "b"], test_case)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use gpui::keymap::Context;
|
use gpui::keymap_matcher::KeymapContext;
|
||||||
use language::CursorShape;
|
use language::CursorShape;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -29,6 +29,8 @@ pub enum Operator {
|
||||||
Delete,
|
Delete,
|
||||||
Yank,
|
Yank,
|
||||||
Object { around: bool },
|
Object { around: bool },
|
||||||
|
FindForward { before: bool },
|
||||||
|
FindBackward { after: bool },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -54,6 +56,10 @@ impl VimState {
|
||||||
|
|
||||||
pub fn vim_controlled(&self) -> bool {
|
pub fn vim_controlled(&self) -> bool {
|
||||||
!matches!(self.mode, Mode::Insert)
|
!matches!(self.mode, Mode::Insert)
|
||||||
|
|| matches!(
|
||||||
|
self.operator_stack.last(),
|
||||||
|
Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. })
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clip_at_line_end(&self) -> bool {
|
pub fn clip_at_line_end(&self) -> bool {
|
||||||
|
@ -64,8 +70,8 @@ impl VimState {
|
||||||
!matches!(self.mode, Mode::Visual { .. })
|
!matches!(self.mode, Mode::Visual { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keymap_context_layer(&self) -> Context {
|
pub fn keymap_context_layer(&self) -> KeymapContext {
|
||||||
let mut context = Context::default();
|
let mut context = KeymapContext::default();
|
||||||
context.map.insert(
|
context.map.insert(
|
||||||
"vim_mode".to_string(),
|
"vim_mode".to_string(),
|
||||||
match self.mode {
|
match self.mode {
|
||||||
|
@ -81,34 +87,48 @@ impl VimState {
|
||||||
}
|
}
|
||||||
|
|
||||||
let active_operator = self.operator_stack.last();
|
let active_operator = self.operator_stack.last();
|
||||||
if matches!(active_operator, Some(Operator::Object { .. })) {
|
|
||||||
context.set.insert("VimObject".to_string());
|
if let Some(active_operator) = active_operator {
|
||||||
|
for context_flag in active_operator.context_flags().into_iter() {
|
||||||
|
context.set.insert(context_flag.to_string());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Operator::set_context(active_operator, &mut context);
|
context.map.insert(
|
||||||
|
"vim_operator".to_string(),
|
||||||
|
active_operator
|
||||||
|
.map(|op| op.id())
|
||||||
|
.unwrap_or_else(|| "none")
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
context
|
context
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Operator {
|
impl Operator {
|
||||||
pub fn set_context(operator: Option<&Operator>, context: &mut Context) {
|
pub fn id(&self) -> &'static str {
|
||||||
let operator_context = match operator {
|
match self {
|
||||||
Some(Operator::Number(_)) => "n",
|
Operator::Number(_) => "n",
|
||||||
Some(Operator::Namespace(Namespace::G)) => "g",
|
Operator::Namespace(Namespace::G) => "g",
|
||||||
Some(Operator::Namespace(Namespace::Z)) => "z",
|
Operator::Namespace(Namespace::Z) => "z",
|
||||||
Some(Operator::Object { around: false }) => "i",
|
Operator::Object { around: false } => "i",
|
||||||
Some(Operator::Object { around: true }) => "a",
|
Operator::Object { around: true } => "a",
|
||||||
Some(Operator::Change) => "c",
|
Operator::Change => "c",
|
||||||
Some(Operator::Delete) => "d",
|
Operator::Delete => "d",
|
||||||
Some(Operator::Yank) => "y",
|
Operator::Yank => "y",
|
||||||
|
Operator::FindForward { before: false } => "f",
|
||||||
None => "none",
|
Operator::FindForward { before: true } => "t",
|
||||||
|
Operator::FindBackward { after: false } => "F",
|
||||||
|
Operator::FindBackward { after: true } => "T",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.to_owned();
|
|
||||||
|
|
||||||
context
|
pub fn context_flags(&self) -> &'static [&'static str] {
|
||||||
.map
|
match self {
|
||||||
.insert("vim_operator".to_string(), operator_context);
|
Operator::Object { .. } => &["VimObject"],
|
||||||
|
Operator::FindForward { .. } | Operator::FindBackward { .. } => &["VimWaiting"],
|
||||||
|
_ => &[],
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use async_compat::Compat;
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
#[cfg(feature = "neovim")]
|
#[cfg(feature = "neovim")]
|
||||||
use gpui::keymap::Keystroke;
|
use gpui::keymap_matcher::Keystroke;
|
||||||
|
|
||||||
use language::{Point, Selection};
|
use language::{Point, Selection};
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ use editor::{Bias, Cancel, Editor};
|
||||||
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
|
use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
|
||||||
use language::CursorShape;
|
use language::CursorShape;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use state::{Mode, Operator, VimState};
|
use state::{Mode, Operator, VimState};
|
||||||
use workspace::{self, Workspace};
|
use workspace::{self, Workspace};
|
||||||
|
@ -55,7 +54,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
|
|
||||||
// Editor Actions
|
// Editor Actions
|
||||||
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
|
||||||
// If we are in a non normal mode or have an active operator, swap to normal mode
|
// 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
|
// Otherwise forward cancel on to the editor
|
||||||
let vim = Vim::read(cx);
|
let vim = Vim::read(cx);
|
||||||
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
|
if vim.state.mode != Mode::Normal || vim.active_operator().is_some() {
|
||||||
|
@ -81,17 +80,21 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Any keystrokes not mapped to vim should clear the active operator
|
|
||||||
pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
|
pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
|
||||||
cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
|
cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
|
||||||
if let Some(handled_by) = handled_by {
|
if let Some(handled_by) = handled_by {
|
||||||
if handled_by.namespace() == "vim" {
|
// 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")
|
||||||
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
if vim.active_operator().is_some() {
|
if vim.active_operator().is_some() {
|
||||||
|
// If the keystroke is not handled by vim, we should clear the operator
|
||||||
vim.clear_operator(cx);
|
vim.clear_operator(cx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
1
crates/vim/test_data/test_capital_f_and_capital_t.json
Normal file
1
crates/vim/test_data/test_capital_f_and_capital_t.json
Normal file
File diff suppressed because one or more lines are too long
1
crates/vim/test_data/test_f_and_t.json
Normal file
1
crates/vim/test_data/test_f_and_t.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -44,6 +44,8 @@ actions!(
|
||||||
ActivateLastItem,
|
ActivateLastItem,
|
||||||
CloseActiveItem,
|
CloseActiveItem,
|
||||||
CloseInactiveItems,
|
CloseInactiveItems,
|
||||||
|
CloseCleanItems,
|
||||||
|
CloseAllItems,
|
||||||
ReopenClosedItem,
|
ReopenClosedItem,
|
||||||
SplitLeft,
|
SplitLeft,
|
||||||
SplitUp,
|
SplitUp,
|
||||||
|
@ -122,6 +124,8 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||||
});
|
});
|
||||||
cx.add_async_action(Pane::close_active_item);
|
cx.add_async_action(Pane::close_active_item);
|
||||||
cx.add_async_action(Pane::close_inactive_items);
|
cx.add_async_action(Pane::close_inactive_items);
|
||||||
|
cx.add_async_action(Pane::close_clean_items);
|
||||||
|
cx.add_async_action(Pane::close_all_items);
|
||||||
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
|
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
|
||||||
let pane = action.pane.upgrade(cx)?;
|
let pane = action.pane.upgrade(cx)?;
|
||||||
let task = Pane::close_item(workspace, pane, action.item_id, cx);
|
let task = Pane::close_item(workspace, pane, action.item_id, cx);
|
||||||
|
@ -258,6 +262,13 @@ pub enum ReorderBehavior {
|
||||||
MoveToIndex(usize),
|
MoveToIndex(usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ItemType {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Clean,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
impl Pane {
|
impl Pane {
|
||||||
pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
|
pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
|
||||||
let handle = cx.weak_handle();
|
let handle = cx.weak_handle();
|
||||||
|
@ -696,41 +707,68 @@ impl Pane {
|
||||||
_: &CloseActiveItem,
|
_: &CloseActiveItem,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> Option<Task<Result<()>>> {
|
) -> Option<Task<Result<()>>> {
|
||||||
let pane_handle = workspace.active_pane().clone();
|
Self::close_main(workspace, ItemType::Active, cx)
|
||||||
let pane = pane_handle.read(cx);
|
|
||||||
if pane.items.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
let item_id_to_close = pane.items[pane.active_item_index].id();
|
|
||||||
let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
|
|
||||||
item_id == item_id_to_close
|
|
||||||
});
|
|
||||||
Some(cx.foreground().spawn(async move {
|
|
||||||
task.await?;
|
|
||||||
Ok(())
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_inactive_items(
|
pub fn close_inactive_items(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_: &CloseInactiveItems,
|
_: &CloseInactiveItems,
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> Option<Task<Result<()>>> {
|
||||||
|
Self::close_main(workspace, ItemType::Inactive, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_all_items(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: &CloseAllItems,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> Option<Task<Result<()>>> {
|
||||||
|
Self::close_main(workspace, ItemType::All, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close_clean_items(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: &CloseCleanItems,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) -> Option<Task<Result<()>>> {
|
||||||
|
Self::close_main(workspace, ItemType::Clean, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_main(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
close_item_type: ItemType,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
) -> Option<Task<Result<()>>> {
|
) -> Option<Task<Result<()>>> {
|
||||||
let pane_handle = workspace.active_pane().clone();
|
let pane_handle = workspace.active_pane().clone();
|
||||||
let pane = pane_handle.read(cx);
|
let pane = pane_handle.read(cx);
|
||||||
if pane.items.is_empty() {
|
if pane.items.is_empty() {
|
||||||
None
|
return None;
|
||||||
} else {
|
}
|
||||||
|
|
||||||
let active_item_id = pane.items[pane.active_item_index].id();
|
let active_item_id = pane.items[pane.active_item_index].id();
|
||||||
|
let clean_item_ids: Vec<_> = pane
|
||||||
|
.items()
|
||||||
|
.filter(|item| !item.is_dirty(cx))
|
||||||
|
.map(|item| item.id())
|
||||||
|
.collect();
|
||||||
let task =
|
let task =
|
||||||
Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
|
Self::close_items(
|
||||||
|
workspace,
|
||||||
|
pane_handle,
|
||||||
|
cx,
|
||||||
|
move |item_id| match close_item_type {
|
||||||
|
ItemType::Active => item_id == active_item_id,
|
||||||
|
ItemType::Inactive => item_id != active_item_id,
|
||||||
|
ItemType::Clean => clean_item_ids.contains(&item_id),
|
||||||
|
ItemType::All => true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Some(cx.foreground().spawn(async move {
|
Some(cx.foreground().spawn(async move {
|
||||||
task.await?;
|
task.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
pub fn close_item(
|
pub fn close_item(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
|
|
|
@ -33,6 +33,7 @@ use gpui::{
|
||||||
actions,
|
actions,
|
||||||
elements::*,
|
elements::*,
|
||||||
impl_actions, impl_internal_actions,
|
impl_actions, impl_internal_actions,
|
||||||
|
keymap_matcher::KeymapContext,
|
||||||
platform::{CursorStyle, WindowOptions},
|
platform::{CursorStyle, WindowOptions},
|
||||||
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||||
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
|
MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View,
|
||||||
|
@ -95,7 +96,7 @@ actions!(
|
||||||
ToggleLeftSidebar,
|
ToggleLeftSidebar,
|
||||||
ToggleRightSidebar,
|
ToggleRightSidebar,
|
||||||
NewTerminal,
|
NewTerminal,
|
||||||
NewSearch,
|
NewSearch
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2142,7 +2143,6 @@ impl Workspace {
|
||||||
let call = self.active_call()?;
|
let call = self.active_call()?;
|
||||||
let room = call.read(cx).room()?.read(cx);
|
let room = call.read(cx).room()?.read(cx);
|
||||||
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
let participant = room.remote_participant_for_peer_id(leader_id)?;
|
||||||
|
|
||||||
let mut items_to_add = Vec::new();
|
let mut items_to_add = Vec::new();
|
||||||
match participant.location {
|
match participant.location {
|
||||||
call::ParticipantLocation::SharedProject { project_id } => {
|
call::ParticipantLocation::SharedProject { project_id } => {
|
||||||
|
@ -2153,6 +2153,12 @@ impl Workspace {
|
||||||
.and_then(|id| state.items_by_leader_view_id.get(&id))
|
.and_then(|id| state.items_by_leader_view_id.get(&id))
|
||||||
{
|
{
|
||||||
items_to_add.push((pane.clone(), item.boxed_clone()));
|
items_to_add.push((pane.clone(), item.boxed_clone()));
|
||||||
|
} else {
|
||||||
|
if let Some(shared_screen) =
|
||||||
|
self.shared_screen_for_peer(leader_id, pane, cx)
|
||||||
|
{
|
||||||
|
items_to_add.push((pane.clone(), Box::new(shared_screen)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2588,7 +2594,7 @@ impl View for Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context {
|
fn keymap_context(&self, _: &AppContext) -> KeymapContext {
|
||||||
let mut keymap = Self::default_keymap_context();
|
let mut keymap = Self::default_keymap_context();
|
||||||
if self.active_pane() == self.dock_pane() {
|
if self.active_pane() == self.dock_pane() {
|
||||||
keymap.set.insert("Dock".into());
|
keymap.set.insert("Dock".into());
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.68.0"
|
version = "0.69.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
|
@ -30,6 +30,7 @@ clock = { path = "../clock" }
|
||||||
diagnostics = { path = "../diagnostics" }
|
diagnostics = { path = "../diagnostics" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
file_finder = { path = "../file_finder" }
|
file_finder = { path = "../file_finder" }
|
||||||
|
human_bytes = "0.4.1"
|
||||||
search = { path = "../search" }
|
search = { path = "../search" }
|
||||||
fs = { path = "../fs" }
|
fs = { path = "../fs" }
|
||||||
fsevent = { path = "../fsevent" }
|
fsevent = { path = "../fsevent" }
|
||||||
|
@ -48,6 +49,7 @@ recent_projects = { path = "../recent_projects" }
|
||||||
rpc = { path = "../rpc" }
|
rpc = { path = "../rpc" }
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
sum_tree = { path = "../sum_tree" }
|
sum_tree = { path = "../sum_tree" }
|
||||||
|
sysinfo = "0.27.1"
|
||||||
text = { path = "../text" }
|
text = { path = "../text" }
|
||||||
terminal_view = { path = "../terminal_view" }
|
terminal_view = { path = "../terminal_view" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
|
@ -108,6 +110,7 @@ tree-sitter-html = "0.19.0"
|
||||||
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"}
|
tree-sitter-scheme = { git = "https://github.com/6cdh/tree-sitter-scheme", rev = "af0fd1fa452cb2562dc7b5c8a8c55551c39273b9"}
|
||||||
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
|
tree-sitter-racket = { git = "https://github.com/zed-industries/tree-sitter-racket", rev = "eb010cf2c674c6fd9a6316a84e28ef90190fe51a"}
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
urlencoding = "2.1.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
call = { path = "../call", features = ["test-support"] }
|
call = { path = "../call", features = ["test-support"] }
|
||||||
|
|
|
@ -50,14 +50,14 @@ impl LspAdapter for RubyLanguageServer {
|
||||||
grammar.highlight_id_for_name("type")?
|
grammar.highlight_id_for_name("type")?
|
||||||
}
|
}
|
||||||
lsp::CompletionItemKind::KEYWORD => {
|
lsp::CompletionItemKind::KEYWORD => {
|
||||||
if label.starts_with(":") {
|
if label.starts_with(':') {
|
||||||
grammar.highlight_id_for_name("string.special.symbol")?
|
grammar.highlight_id_for_name("string.special.symbol")?
|
||||||
} else {
|
} else {
|
||||||
grammar.highlight_id_for_name("keyword")?
|
grammar.highlight_id_for_name("keyword")?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lsp::CompletionItemKind::VARIABLE => {
|
lsp::CompletionItemKind::VARIABLE => {
|
||||||
if label.starts_with("@") {
|
if label.starts_with('@') {
|
||||||
grammar.highlight_id_for_name("property")?
|
grammar.highlight_id_for_name("property")?
|
||||||
} else {
|
} else {
|
||||||
return None;
|
return None;
|
||||||
|
|
|
@ -128,8 +128,14 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||||
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
|
Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"),
|
||||||
_ => None,
|
_ => None,
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
|
let text = match &item.detail {
|
||||||
|
Some(detail) => format!("{} {}", item.label, detail),
|
||||||
|
None => item.label.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
Some(language::CodeLabel {
|
Some(language::CodeLabel {
|
||||||
text: item.label.clone(),
|
text,
|
||||||
runs: vec![(0..len, highlight_id)],
|
runs: vec![(0..len, highlight_id)],
|
||||||
filter_range: 0..len,
|
filter_range: 0..len,
|
||||||
})
|
})
|
||||||
|
|
|
@ -101,7 +101,7 @@ fn main() {
|
||||||
|
|
||||||
//Setup settings global before binding actions
|
//Setup settings global before binding actions
|
||||||
cx.set_global(SettingsFile::new(
|
cx.set_global(SettingsFile::new(
|
||||||
&*paths::SETTINGS,
|
&paths::SETTINGS,
|
||||||
settings_file_content.clone(),
|
settings_file_content.clone(),
|
||||||
fs.clone(),
|
fs.clone(),
|
||||||
));
|
));
|
||||||
|
@ -586,7 +586,7 @@ async fn handle_cli_connection(
|
||||||
|
|
||||||
responses
|
responses
|
||||||
.send(CliResponse::Exit {
|
.send(CliResponse::Exit {
|
||||||
status: if errored { 1 } else { 0 },
|
status: i32::from(errored),
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
|
@ -338,18 +338,25 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||||
action: Box::new(crate::OpenTelemetryLog),
|
action: Box::new(crate::OpenTelemetryLog),
|
||||||
},
|
},
|
||||||
MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Copy System Specs Into Clipboard",
|
||||||
|
action: Box::new(crate::CopySystemSpecsIntoClipboard),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "File Bug Report",
|
||||||
|
action: Box::new(crate::FileBugReport),
|
||||||
|
},
|
||||||
|
MenuItem::Action {
|
||||||
|
name: "Request Feature",
|
||||||
|
action: Box::new(crate::RequestFeature),
|
||||||
|
},
|
||||||
|
MenuItem::Separator,
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Documentation",
|
name: "Documentation",
|
||||||
action: Box::new(crate::OpenBrowser {
|
action: Box::new(crate::OpenBrowser {
|
||||||
url: "https://zed.dev/docs".into(),
|
url: "https://zed.dev/docs".into(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
MenuItem::Action {
|
|
||||||
name: "Give Feedback",
|
|
||||||
action: Box::new(crate::OpenBrowser {
|
|
||||||
url: super::feedback::NEW_ISSUE_URL.into(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
MenuItem::Action {
|
MenuItem::Action {
|
||||||
name: "Zed Twitter",
|
name: "Zed Twitter",
|
||||||
action: Box::new(crate::OpenBrowser {
|
action: Box::new(crate::OpenBrowser {
|
||||||
|
|
52
crates/zed/src/system_specs.rs
Normal file
52
crates/zed/src/system_specs.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use std::{env, fmt::Display};
|
||||||
|
|
||||||
|
use gpui::AppContext;
|
||||||
|
use human_bytes::human_bytes;
|
||||||
|
use sysinfo::{System, SystemExt};
|
||||||
|
use util::channel::ReleaseChannel;
|
||||||
|
|
||||||
|
pub struct SystemSpecs {
|
||||||
|
app_version: &'static str,
|
||||||
|
release_channel: &'static str,
|
||||||
|
os_name: &'static str,
|
||||||
|
os_version: Option<String>,
|
||||||
|
memory: u64,
|
||||||
|
architecture: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemSpecs {
|
||||||
|
pub fn new(cx: &AppContext) -> Self {
|
||||||
|
let platform = cx.platform();
|
||||||
|
let system = System::new_all();
|
||||||
|
|
||||||
|
SystemSpecs {
|
||||||
|
app_version: env!("CARGO_PKG_VERSION"),
|
||||||
|
release_channel: cx.global::<ReleaseChannel>().dev_name(),
|
||||||
|
os_name: platform.os_name(),
|
||||||
|
os_version: platform
|
||||||
|
.os_version()
|
||||||
|
.ok()
|
||||||
|
.map(|os_version| os_version.to_string()),
|
||||||
|
memory: system.total_memory(),
|
||||||
|
architecture: env::consts::ARCH,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SystemSpecs {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let os_information = match &self.os_version {
|
||||||
|
Some(os_version) => format!("OS: {} {}", self.os_name, os_version),
|
||||||
|
None => format!("OS: {}", self.os_name),
|
||||||
|
};
|
||||||
|
let system_specs = [
|
||||||
|
format!("Zed: {} ({})", self.app_version, self.release_channel),
|
||||||
|
os_information,
|
||||||
|
format!("Memory: {}", human_bytes(self.memory as f64)),
|
||||||
|
format!("Architecture: {}", self.architecture),
|
||||||
|
]
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
write!(f, "{system_specs}")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
mod feedback;
|
mod feedback;
|
||||||
pub mod languages;
|
pub mod languages;
|
||||||
pub mod menus;
|
pub mod menus;
|
||||||
|
pub mod system_specs;
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test;
|
pub mod test;
|
||||||
|
|
||||||
|
@ -21,7 +22,7 @@ use gpui::{
|
||||||
},
|
},
|
||||||
impl_actions,
|
impl_actions,
|
||||||
platform::{WindowBounds, WindowOptions},
|
platform::{WindowBounds, WindowOptions},
|
||||||
AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
|
AssetSource, AsyncAppContext, ClipboardItem, TitlebarOptions, ViewContext, WindowKind,
|
||||||
};
|
};
|
||||||
use language::Rope;
|
use language::Rope;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
@ -33,6 +34,7 @@ use serde::Deserialize;
|
||||||
use serde_json::to_string_pretty;
|
use serde_json::to_string_pretty;
|
||||||
use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
|
use settings::{keymap_file_json_schema, settings_file_json_schema, Settings};
|
||||||
use std::{env, path::Path, str, sync::Arc};
|
use std::{env, path::Path, str, sync::Arc};
|
||||||
|
use system_specs::SystemSpecs;
|
||||||
use util::{channel::ReleaseChannel, paths, ResultExt};
|
use util::{channel::ReleaseChannel, paths, ResultExt};
|
||||||
pub use workspace;
|
pub use workspace;
|
||||||
use workspace::{sidebar::SidebarSide, AppState, Workspace};
|
use workspace::{sidebar::SidebarSide, AppState, Workspace};
|
||||||
|
@ -67,6 +69,9 @@ actions!(
|
||||||
ResetBufferFontSize,
|
ResetBufferFontSize,
|
||||||
InstallCommandLineInterface,
|
InstallCommandLineInterface,
|
||||||
ResetDatabase,
|
ResetDatabase,
|
||||||
|
CopySystemSpecsIntoClipboard,
|
||||||
|
RequestFeature,
|
||||||
|
FileBugReport
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -245,6 +250,41 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace, _: &CopySystemSpecsIntoClipboard, cx: &mut ViewContext<Workspace>| {
|
||||||
|
let system_specs = SystemSpecs::new(cx).to_string();
|
||||||
|
let item = ClipboardItem::new(system_specs.clone());
|
||||||
|
cx.prompt(
|
||||||
|
gpui::PromptLevel::Info,
|
||||||
|
&format!("Copied into clipboard:\n\n{system_specs}"),
|
||||||
|
&["OK"],
|
||||||
|
);
|
||||||
|
cx.write_to_clipboard(item);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace, _: &RequestFeature, cx: &mut ViewContext<Workspace>| {
|
||||||
|
let url = "https://github.com/zed-industries/feedback/issues/new?assignees=&labels=enhancement%2Ctriage&template=0_feature_request.yml";
|
||||||
|
cx.dispatch_action(OpenBrowser {
|
||||||
|
url: url.into(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.add_action(
|
||||||
|
|_: &mut Workspace, _: &FileBugReport, cx: &mut ViewContext<Workspace>| {
|
||||||
|
let system_specs_text = SystemSpecs::new(cx).to_string();
|
||||||
|
let url = format!(
|
||||||
|
"https://github.com/zed-industries/feedback/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml&environment={}",
|
||||||
|
urlencoding::encode(&system_specs_text)
|
||||||
|
);
|
||||||
|
cx.dispatch_action(OpenBrowser {
|
||||||
|
url: url.into(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
activity_indicator::init(cx);
|
activity_indicator::init(cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
settings::KeymapFileContent::load_defaults(cx);
|
settings::KeymapFileContent::load_defaults(cx);
|
||||||
|
@ -298,11 +338,11 @@ pub fn initialize_workspace(
|
||||||
},
|
},
|
||||||
"schemas": [
|
"schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": [schema_file_match(&*paths::SETTINGS)],
|
"fileMatch": [schema_file_match(&paths::SETTINGS)],
|
||||||
"schema": settings_file_json_schema(theme_names, language_names),
|
"schema": settings_file_json_schema(theme_names, language_names),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fileMatch": [schema_file_match(&*paths::KEYMAP)],
|
"fileMatch": [schema_file_match(&paths::KEYMAP)],
|
||||||
"schema": keymap_file_json_schema(&action_names),
|
"schema": keymap_file_json_schema(&action_names),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -606,7 +646,7 @@ fn open_bundled_config_file(
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
workspace
|
workspace
|
||||||
.with_local_workspace(&app_state.clone(), cx, |workspace, cx| {
|
.with_local_workspace(&app_state, cx, |workspace, cx| {
|
||||||
let project = workspace.project().clone();
|
let project = workspace.project().clone();
|
||||||
let buffer = project.update(cx, |project, cx| {
|
let buffer = project.update(cx, |project, cx| {
|
||||||
let text = Assets::get(asset_path).unwrap().data;
|
let text = Assets::get(asset_path).unwrap().data;
|
||||||
|
|
|
@ -32,13 +32,13 @@ export default function contactNotification(colorScheme: ColorScheme): Object {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
dismissButton: {
|
dismissButton: {
|
||||||
color: foreground(layer, "on"),
|
color: foreground(layer, "variant"),
|
||||||
iconWidth: 8,
|
iconWidth: 8,
|
||||||
iconHeight: 8,
|
iconHeight: 8,
|
||||||
buttonWidth: 8,
|
buttonWidth: 8,
|
||||||
buttonHeight: 8,
|
buttonHeight: 8,
|
||||||
hover: {
|
hover: {
|
||||||
color: foreground(layer, "on", "hovered"),
|
color: foreground(layer, "hovered"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -257,7 +257,6 @@ export default function editor(colorScheme: ColorScheme) {
|
||||||
right: 6,
|
right: 6,
|
||||||
},
|
},
|
||||||
hover: {
|
hover: {
|
||||||
color: foreground(layer, "on", "hovered"),
|
|
||||||
background: background(layer, "on", "hovered"),
|
background: background(layer, "on", "hovered"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -80,5 +80,17 @@ export default function search(colorScheme: ColorScheme) {
|
||||||
...text(layer, "mono", "on"),
|
...text(layer, "mono", "on"),
|
||||||
size: 18,
|
size: 18,
|
||||||
},
|
},
|
||||||
|
dismissButton: {
|
||||||
|
color: foreground(layer, "variant"),
|
||||||
|
iconWidth: 12,
|
||||||
|
buttonWidth: 14,
|
||||||
|
padding: {
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
color: foreground(layer, "hovered"),
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function tabBar(colorScheme: ColorScheme) {
|
||||||
// Close icons
|
// Close icons
|
||||||
iconWidth: 8,
|
iconWidth: 8,
|
||||||
iconClose: foreground(layer, "variant"),
|
iconClose: foreground(layer, "variant"),
|
||||||
iconCloseActive: foreground(layer),
|
iconCloseActive: foreground(layer, "hovered"),
|
||||||
|
|
||||||
// Indicators
|
// Indicators
|
||||||
iconConflict: foreground(layer, "warning"),
|
iconConflict: foreground(layer, "warning"),
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue