Compare commits

..

23 Commits

Author SHA1 Message Date
Mikayla
b93df56170 Add admin APIs for interacting and viewing feature flags 2023-09-10 23:38:24 -07:00
Conrad Irwin
7cc05c99c2 Update getting started
Just ran through this again.
2023-09-08 23:46:12 -06:00
Conrad Irwin
e29ce489c8 vim: Add ZZ and ZQ (#2950)
The major change here is a refactoring to allow controling the save
behaviour when closing items, which is pre-work needed for vim command
palette.

For zed-industries/community#1868

Release Notes:

- vim: Add `ZZ` and `ZQ` to close the current item.
([#1868](https://github.com/zed-industries/community/issues/1868))
2023-09-08 16:58:04 -06:00
Conrad Irwin
4c92172cca Partially roll back refactoring 2023-09-08 16:49:50 -06:00
Conrad Irwin
ba1c350dad vim: Add ZZ and ZQ
The major change here is a refactoring to allow controling the save
behaviour when closing items, which is pre-work needed for vim command
palette.

For zed-industries/community#1868
2023-09-08 16:25:20 -06:00
Conrad Irwin
5d782b6cf0 vim . to replay (#2936)
Release Notes:

- vim: Add `.` to replay
([#946](https://github.com/zed-industries/community/issues/946))
- vim: Fix `J` in visual mode, and with counts.
2023-09-08 11:52:35 -06:00
Conrad Irwin
88dae22e3e Don't replay ShowCharacterPalette 2023-09-08 11:35:00 -06:00
Conrad Irwin
f069cd0485 Fix f,t on soft-wrapped lines (#2940)
Release Notes:

- vim: fix `f` and `t` on softwrapped lines
2023-09-08 11:34:12 -06:00
Joseph T. Lyons
e1d4d911b4 Add tooltip to language selector (#2949)
Release Notes:

- N/A
2023-09-08 12:48:37 -04:00
Joseph T. Lyons
a0701777d5 Make tooltip title case to match other tooltips 2023-09-08 12:44:49 -04:00
Joseph T. Lyons
f4a9d3f269 Add tooltip to language selector 2023-09-08 12:41:32 -04:00
Julia
87472a9de6 Fix Python's cached binary retrieval being borked (#2948)
We fixed this while brainstorming a better approach to handle server
binaries and if we already have a fix for this one then we might as well
have this not be broken while the new mechanism is being built

Release Notes:

- Fixed Python language server not launching without a network
connection.
2023-09-08 12:21:18 -04:00
Conrad Irwin
5f897f45a8 Fix f,t on soft-wrapped lines
Also remove the (dangerously confusing) display_map.find_while
2023-09-08 10:16:46 -06:00
Julia
74ccb3df63 Fix Python's cached binary retrieval being borked
Co-Authored-By: Max Brunsfeld <max@zed.dev>
2023-09-08 12:09:31 -04:00
Antonio Scandurra
e9747d0fea Find keystrokes defined on a child but handled by a parent (#2947)
This fixes a bug that was preventing keystrokes from being shown on
tooltips for the "Buffer Search" and "Inline Assist" buttons in the
toolbar.

This pull request makes the behavior of `keystrokes_for_action` more
consistent with the behavior of `available_actions`. It seems reasonable
that, if a child view defines a keystroke for an action and that action
is handled on a parent, we should show the child's keystroke.

Release Notes:

- Fixed a bug that was preventing certain keystrokes from being shown in
tooltips.
2023-09-08 14:11:30 +02:00
Antonio Scandurra
ddc8a126da Find keystrokes defined on a child but handled by a parent
This fixes a bug that was preventing keystrokes from being shown on tooltips
for the "Buffer Search" and "Inline Assist" buttons in the toolbar.

This commit makes the behavior of `keystrokes_for_action` more consistent with
the behavior of `available_actions`. It seems reasonable that, if a child view
defines a keystroke for an action and that action is handled on a parent, we
should show the child's keystroke.
2023-09-08 12:50:59 +02:00
Antonio Scandurra
6ad2ec4825 Make channel notes act as an editor to enable inline assistant (#2946)
I think it should be fine to make channel notes act as editors, so I'll
go ahead and merge this but cc'ing @mikayla-maki and @maxbrunsfeld, in
case I'm overlooking something.

Release Notes:

- Added the inline assistant to channel notes.
2023-09-08 11:51:14 +02:00
Antonio Scandurra
4e818fed4a Make channel notes act as an editor to enable inline assistant 2023-09-08 11:20:49 +02:00
Conrad Irwin
8e2e00e003 add vim-specific J (with repeatability) 2023-09-07 11:08:07 -06:00
Conrad Irwin
48bb2a3321 TEMP 2023-09-07 10:51:18 -06:00
Conrad Irwin
1b1d7f22cc Add visual area repeating 2023-09-07 10:45:38 -06:00
Conrad Irwin
f22d53eef9 Make test more deterministic
Otherwise these pass only when --features=neovim is set
2023-09-06 14:14:49 -06:00
Conrad Irwin
20f98e4d17 vim . to replay
Co-Authored-By: maxbrunsfeld@gmail.com
2023-09-06 13:49:55 -06:00
50 changed files with 1578 additions and 946 deletions

419
Cargo.lock generated
View File

@@ -550,7 +550,7 @@ dependencies = [
"libc",
"pin-project",
"redox_syscall 0.2.16",
"xattr 0.2.3",
"xattr",
]
[[package]]
@@ -974,7 +974,7 @@ dependencies = [
"collections",
"editor",
"gpui",
"itertools 0.10.5",
"itertools",
"language",
"outline",
"project",
@@ -1924,7 +1924,7 @@ dependencies = [
"cranelift-codegen",
"cranelift-entity",
"cranelift-frontend",
"itertools 0.10.5",
"itertools",
"log",
"smallvec",
"wasmparser",
@@ -2008,12 +2008,6 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -2074,41 +2068,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "darling"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 1.0.109",
]
[[package]]
name = "darling_macro"
version = "0.14.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
dependencies = [
"darling_core",
"quote",
"syn 1.0.109",
]
[[package]]
name = "dashmap"
version = "5.5.1"
@@ -2178,37 +2137,6 @@ dependencies = [
"serde",
]
[[package]]
name = "derive_builder"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "derive_builder_macro"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
dependencies = [
"derive_builder_core",
"syn 1.0.109",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@@ -2417,7 +2345,7 @@ dependencies = [
"git",
"gpui",
"indoc",
"itertools 0.10.5",
"itertools",
"language",
"lazy_static",
"log",
@@ -2546,12 +2474,6 @@ dependencies = [
"libc",
]
[[package]]
name = "esaxx-rs"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f748b253ceca9fed5f42f8b5ceb3851e93102199bc25b64b65369f76e5c0a35"
[[package]]
name = "etagere"
version = "0.2.8"
@@ -3213,7 +3135,7 @@ dependencies = [
"futures 0.3.28",
"gpui_macros",
"image",
"itertools 0.10.5",
"itertools",
"lazy_static",
"log",
"media",
@@ -3284,16 +3206,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "half"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872"
dependencies = [
"cfg-if 1.0.0",
"crunchy",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
@@ -3589,12 +3501,6 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.4.0"
@@ -3803,24 +3709,6 @@ dependencies = [
"waker-fn",
]
[[package]]
name = "itertools"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -4317,22 +4205,6 @@ dependencies = [
"libc",
]
[[package]]
name = "macro_rules_attribute"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf0c9b980bf4f3a37fd7b1c066941dd1b1d0152ce6ee6e8fe8c49b9f6810d862"
dependencies = [
"macro_rules_attribute-proc_macro",
"paste",
]
[[package]]
name = "macro_rules_attribute-proc_macro"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58093314a45e00c77d5c508f76e77c3396afbbc0d01506e7fae47b018bac2b1d"
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -4602,27 +4474,6 @@ dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "monostate"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f370ae88093ec6b11a710dec51321a61d420fafd1bad6e30d01bd9c920e8ee"
dependencies = [
"monostate-impl",
"serde",
]
[[package]]
name = "monostate-impl"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "371717c0a5543d6a800cac822eac735aa7d2d2fbb41002e9856a4089532dbdce"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.29",
]
[[package]]
name = "more-asserts"
version = "0.2.2"
@@ -4662,19 +4513,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ndarray"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32"
dependencies = [
"matrixmultiply",
"num-complex 0.4.4",
"num-integer",
"num-traits",
"rawpointer",
]
[[package]]
name = "ndk"
version = "0.7.0"
@@ -4802,7 +4640,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36"
dependencies = [
"num-bigint 0.2.6",
"num-complex 0.2.4",
"num-complex",
"num-integer",
"num-iter",
"num-rational 0.2.4",
@@ -4858,15 +4696,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-complex"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214"
dependencies = [
"num-traits",
]
[[package]]
name = "num-derive"
version = "0.3.3"
@@ -5047,28 +4876,6 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "onig"
version = "6.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f"
dependencies = [
"bitflags 1.3.2",
"libc",
"once_cell",
"onig_sys",
]
[[package]]
name = "onig_sys"
version = "69.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "opaque-debug"
version = "0.3.0"
@@ -5128,26 +4935,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "ort"
version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e56c9c4185ee949ef961aca8777d1dbd52cb104b444669adad63e8181820a7"
dependencies = [
"flate2",
"half",
"lazy_static",
"libc",
"ndarray",
"tar",
"thiserror",
"tracing",
"ureq",
"vswhom",
"winapi 0.3.9",
"zip",
]
[[package]]
name = "os_str_bytes"
version = "6.5.1"
@@ -5687,7 +5474,7 @@ dependencies = [
"globset",
"gpui",
"ignore",
"itertools 0.10.5",
"itertools",
"language",
"lazy_static",
"log",
@@ -5811,7 +5598,7 @@ checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5"
dependencies = [
"bytes 1.4.0",
"heck 0.3.3",
"itertools 0.10.5",
"itertools",
"lazy_static",
"log",
"multimap",
@@ -5830,7 +5617,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "600d2f334aa05acb02a755e217ef1ab6dea4d51b58b7846588b747edec04efba"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -5843,7 +5630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools",
"proc-macro2",
"quote",
"syn 1.0.109",
@@ -6072,17 +5859,6 @@ dependencies = [
"rayon-core",
]
[[package]]
name = "rayon-cond"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd1259362c9065e5ea39a789ef40b1e3fd934c94beb7b5ab3ac6629d3b5e7cb7"
dependencies = [
"either",
"itertools 0.8.2",
"rayon",
]
[[package]]
name = "rayon-core"
version = "1.11.0"
@@ -6628,18 +6404,6 @@ dependencies = [
"webpki 0.22.0",
]
[[package]]
name = "rustls"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.4",
"sct 0.7.0",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.3"
@@ -6649,26 +6413,6 @@ dependencies = [
"base64 0.21.2",
]
[[package]]
name = "rustls-webpki"
version = "0.100.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.101.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.14"
@@ -6990,8 +6734,6 @@ dependencies = [
"lazy_static",
"log",
"matrixmultiply",
"ndarray",
"ort",
"parking_lot 0.11.2",
"parse_duration",
"picker",
@@ -7010,7 +6752,6 @@ dependencies = [
"tempdir",
"theme",
"tiktoken-rs 0.5.1",
"tokenizers",
"tree-sitter",
"tree-sitter-cpp",
"tree-sitter-elixir",
@@ -7468,18 +7209,6 @@ dependencies = [
"lock_api",
]
[[package]]
name = "spm_precompiled"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
dependencies = [
"base64 0.13.1",
"nom",
"serde",
"unicode-segmentation",
]
[[package]]
name = "spsc-buffer"
version = "0.1.1"
@@ -7519,7 +7248,7 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e"
dependencies = [
"itertools 0.10.5",
"itertools",
"nom",
"unicode_categories",
]
@@ -7857,17 +7586,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tar"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb"
dependencies = [
"filetime",
"libc",
"xattr 1.0.1",
]
[[package]]
name = "target-lexicon"
version = "0.12.11"
@@ -7916,7 +7634,7 @@ dependencies = [
"dirs 4.0.0",
"futures 0.3.28",
"gpui",
"itertools 0.10.5",
"itertools",
"lazy_static",
"libc",
"mio-extras",
@@ -7947,7 +7665,7 @@ dependencies = [
"editor",
"futures 0.3.28",
"gpui",
"itertools 0.10.5",
"itertools",
"language",
"lazy_static",
"libc",
@@ -8195,37 +7913,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokenizers"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b515a66453a4d68f03398054f7204fd0dde6b93d3f20ea90b08025ab49b499"
dependencies = [
"aho-corasick 0.7.20",
"derive_builder",
"esaxx-rs",
"getrandom 0.2.10",
"itertools 0.9.0",
"lazy_static",
"log",
"macro_rules_attribute",
"monostate",
"onig",
"paste",
"rand 0.8.5",
"rayon",
"rayon-cond",
"regex",
"regex-syntax 0.7.4",
"serde",
"serde_json",
"spm_precompiled",
"thiserror",
"unicode-normalization-alignments",
"unicode-segmentation",
"unicode_categories",
]
[[package]]
name = "tokio"
version = "1.32.0"
@@ -8928,15 +8615,6 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-normalization-alignments"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
dependencies = [
"smallvec",
]
[[package]]
name = "unicode-script"
version = "0.5.5"
@@ -8979,21 +8657,6 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
dependencies = [
"base64 0.21.2",
"log",
"once_cell",
"rustls 0.21.7",
"rustls-webpki 0.100.2",
"url",
"webpki-roots 0.23.1",
]
[[package]]
name = "url"
version = "2.4.0"
@@ -9166,12 +8829,14 @@ dependencies = [
"collections",
"command_palette",
"editor",
"futures 0.3.28",
"gpui",
"indoc",
"itertools 0.10.5",
"itertools",
"language",
"language_selector",
"log",
"lsp",
"nvim-rs",
"parking_lot 0.11.2",
"project",
@@ -9186,26 +8851,6 @@ dependencies = [
"workspace",
]
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18"
dependencies = [
"cc",
"libc",
]
[[package]]
name = "vte"
version = "0.11.1"
@@ -9672,15 +9317,6 @@ dependencies = [
"webpki 0.22.0",
]
[[package]]
name = "webpki-roots"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
dependencies = [
"rustls-webpki 0.100.2",
]
[[package]]
name = "weezl"
version = "0.1.7"
@@ -10029,7 +9665,7 @@ dependencies = [
"gpui",
"indoc",
"install_cli",
"itertools 0.10.5",
"itertools",
"language",
"lazy_static",
"log",
@@ -10077,15 +9713,6 @@ dependencies = [
"libc",
]
[[package]]
name = "xattr"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4686009f71ff3e5c4dbcf1a282d0a44db3f021ba69350cd42086b3e5f1c6985"
dependencies = [
"libc",
]
[[package]]
name = "xmlparser"
version = "0.13.5"
@@ -10289,18 +9916,6 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"byteorder",
"crc32fast",
"crossbeam-utils",
"flate2",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"

View File

@@ -8,7 +8,31 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
### Dependencies
* Install [Postgres.app](https://postgresapp.com) and start it.
* Install Xcode from https://apps.apple.com/us/app/xcode/id497799835?mt=12, and accept the license:
```
sudo xcodebuild -license
```
* Install homebrew, rust and node
```
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
brew install rust
brew install node
```
* Ensure rust executables are in your $PATH
```
echo $HOME/.cargo/bin | sudo tee /etc/paths.d/10-rust
```
* Install postgres and configure the database
```
brew install postgresql@15
brew services start postgresql@15
psql -c "CREATE ROLE postgres SUPERUSER LOGIN" postgres
psql -U postgres -c "CREATE DATABASE zed"
```
* Install the `LiveKit` server and the `foreman` process supervisor:
```
@@ -41,6 +65,17 @@ Welcome to Zed, a lightning-fast, collaborative code editor that makes your drea
GITHUB_TOKEN=<$token> script/bootstrap
```
* Now try running zed with collaboration disabled:
```
cargo run
```
### Common errors
* `xcrun: error: unable to find utility "metal", not a developer tool or in PATH`
* You need to install Xcode and then run: `xcode-select --switch /Applications/Xcode.app/Contents/Developer`
* (see https://github.com/gfx-rs/gfx/issues/2309)
### Testing against locally-running servers
Start the web and collab servers:

View File

@@ -198,6 +198,18 @@
"z c": "editor::Fold",
"z o": "editor::UnfoldLines",
"z f": "editor::FoldSelectedRanges",
"shift-z shift-q": [
"pane::CloseActiveItem",
{
"saveBehavior": "dontSave"
}
],
"shift-z shift-z": [
"pane::CloseActiveItem",
{
"saveBehavior": "promptOnConflict"
}
],
// Count support
"1": [
"vim::Number",
@@ -316,6 +328,7 @@
{
"context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting",
"bindings": {
".": "vim::Repeat",
"c": [
"vim::PushOperator",
"Change"
@@ -326,15 +339,12 @@
"Delete"
],
"shift-d": "vim::DeleteToEndOfLine",
"shift-j": "editor::JoinLines",
"shift-j": "vim::JoinLines",
"y": [
"vim::PushOperator",
"Yank"
],
"i": [
"vim::SwitchMode",
"Insert"
],
"i": "vim::InsertBefore",
"shift-i": "vim::InsertFirstNonWhitespace",
"a": "vim::InsertAfter",
"shift-a": "vim::InsertEndOfLine",
@@ -448,13 +458,12 @@
],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
"shift-r": "vim::SubstituteLine",
"c": "vim::Substitute",
"~": "vim::ChangeCase",
"shift-i": [
"vim::SwitchMode",
"Insert"
],
"shift-i": "vim::InsertBefore",
"shift-a": "vim::InsertAfter",
"shift-j": "vim::JoinLines",
"r": [
"vim::PushOperator",
"Replace"

View File

@@ -1,6 +1,6 @@
use crate::{
auth,
db::{Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
db::{FeatureFlag, FlagId, Invite, NewSignup, NewUserParams, User, UserId, WaitlistSummary},
rpc::{self, ResultExt},
AppState, Error, Result,
};
@@ -26,6 +26,7 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
.route("/users", get(get_users).post(create_user))
.route("/users/:id", put(update_user).delete(destroy_user))
.route("/users/:id/access_tokens", post(create_access_token))
.route("/users/:id/feature_flags", post(add_user_flag))
.route("/users_with_no_invites", get(get_users_with_no_invites))
.route("/invite_codes/:code", get(get_user_for_invite_code))
.route("/panic", post(trace_panic))
@@ -35,6 +36,11 @@ pub fn routes(rpc_server: Arc<rpc::Server>, state: Arc<AppState>) -> Router<Body
.route("/user_invites", post(create_invite_from_code))
.route("/unsent_invites", get(get_unsent_invites))
.route("/sent_invites", post(record_sent_invites))
.route(
"/feature_flags",
get(feature_flags).post(create_feature_flag),
)
.route("/feature_flags/:id", get(users_for_feature_flag))
.layer(
ServiceBuilder::new()
.layer(Extension(state))
@@ -328,6 +334,55 @@ async fn create_access_token(
}))
}
#[derive(Serialize, Deserialize)]
struct FlagIdField {
flag_id: FlagId,
}
async fn add_user_flag(
Path(user_id): Path<UserId>,
Json(params): Json<FlagIdField>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<()> {
let user = app
.db
.get_user_by_id(user_id)
.await?
.ok_or_else(|| anyhow!("user not found"))?;
let user_id = user.id;
app.db.add_user_flag(user_id, params.flag_id).await?;
Ok(())
}
async fn feature_flags(Extension(app): Extension<Arc<AppState>>) -> Result<Json<Vec<FeatureFlag>>> {
Ok(Json(app.db.get_feature_flags().await?))
}
#[derive(Deserialize)]
struct CreateFeatureFlagParam {
flag: String,
}
async fn create_feature_flag(
Json(params): Json<CreateFeatureFlagParam>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<FlagIdField>> {
let id = app.db.create_feature_flag(&params.flag).await?;
Ok(Json(FlagIdField { flag_id: id }))
}
async fn users_for_feature_flag(
Query(params): Query<FlagIdField>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<Vec<User>>> {
let users = app.db.get_flag_users(params.flag_id).await?;
Ok(Json(users))
}
async fn get_user_for_invite_code(
Path(code): Path<String>,
Extension(app): Extension<Arc<AppState>>,

View File

@@ -41,6 +41,7 @@ use tokio::sync::{Mutex, OwnedMutexGuard};
pub use ids::*;
pub use sea_orm::ConnectOptions;
pub use tables::feature_flag::Model as FeatureFlag;
pub use tables::user::Model as User;
pub struct Database {

View File

@@ -241,7 +241,7 @@ impl Database {
result
}
pub async fn create_user_flag(&self, flag: &str) -> Result<FlagId> {
pub async fn create_feature_flag(&self, flag: &str) -> Result<FlagId> {
self.transaction(|tx| async move {
let flag = feature_flag::Entity::insert(feature_flag::ActiveModel {
flag: ActiveValue::set(flag.to_string()),
@@ -256,6 +256,11 @@ impl Database {
.await
}
pub async fn get_feature_flags(&self) -> Result<Vec<FeatureFlag>> {
self.transaction(|tx| async move { Ok(feature_flag::Entity::find().all(&*tx).await?) })
.await
}
pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> {
self.transaction(|tx| async move {
user_feature::Entity::insert(user_feature::ActiveModel {
@@ -270,6 +275,21 @@ impl Database {
.await
}
pub async fn get_flag_users(&self, id: FlagId) -> Result<Vec<User>> {
self.transaction(|tx| async move {
let users = FeatureFlag {
id,
..Default::default()
}
.find_linked(feature_flag::FlaggedUsers)
.all(&*tx)
.await?;
Ok(users)
})
.await
}
pub async fn get_user_flags(&self, user: UserId) -> Result<Vec<String>> {
self.transaction(|tx| async move {
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]

View File

@@ -1,8 +1,9 @@
use sea_orm::entity::prelude::*;
use serde_derive::Serialize;
use crate::db::FlagId;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel, Serialize)]
#[sea_orm(table_name = "feature_flags")]
pub struct Model {
#[sea_orm(primary_key)]

View File

@@ -1,16 +1,16 @@
use crate::{
db::{Database, NewUserParams},
db::{Database, FeatureFlag, NewUserParams},
test_both_dbs,
};
use std::sync::Arc;
test_both_dbs!(
test_get_user_flags,
test_get_user_flags_postgres,
test_get_user_flags_sqlite
test_feature_flags,
test_feature_flags_postgres,
test_feature_flags_sqlite
);
async fn test_get_user_flags(db: &Arc<Database>) {
async fn test_feature_flags(db: &Arc<Database>) {
let user_1 = db
.create_user(
&format!("user1@example.com"),
@@ -42,8 +42,8 @@ async fn test_get_user_flags(db: &Arc<Database>) {
const CHANNELS_ALPHA: &'static str = "channels-alpha";
const NEW_SEARCH: &'static str = "new-search";
let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap();
let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap();
let channels_flag = db.create_feature_flag(CHANNELS_ALPHA).await.unwrap();
let search_flag = db.create_feature_flag(NEW_SEARCH).await.unwrap();
db.add_user_flag(user_1, channels_flag).await.unwrap();
db.add_user_flag(user_1, search_flag).await.unwrap();
@@ -57,4 +57,29 @@ async fn test_get_user_flags(db: &Arc<Database>) {
let mut user_2_flags = db.get_user_flags(user_2).await.unwrap();
user_2_flags.sort();
assert_eq!(user_2_flags, &[CHANNELS_ALPHA]);
let flags = db.get_feature_flags().await.unwrap();
assert_eq!(
flags,
vec![
FeatureFlag {
id: channels_flag,
flag: CHANNELS_ALPHA.to_string(),
},
FeatureFlag {
id: search_flag,
flag: NEW_SEARCH.to_string(),
},
]
);
let users_for_channels_alpha = db
.get_flag_users(channels_flag)
.await
.unwrap()
.into_iter()
.map(|user| user.id)
.collect::<Vec<_>>();
assert_eq!(users_for_channels_alpha, vec![user_1, user_2])
}

View File

@@ -15,7 +15,7 @@ use gpui::{
ViewContext, ViewHandle,
};
use project::Project;
use std::any::Any;
use std::any::{Any, TypeId};
use workspace::{
item::{FollowableItem, Item, ItemHandle},
register_followable_item,
@@ -189,6 +189,21 @@ impl View for ChannelView {
}
impl Item for ChannelView {
fn act_as_type<'a>(
&'a self,
type_id: TypeId,
self_handle: &'a ViewHandle<Self>,
_: &'a AppContext,
) -> Option<&'a AnyViewHandle> {
if type_id == TypeId::of::<Self>() {
Some(self_handle)
} else if type_id == TypeId::of::<Editor>() {
Some(&self.editor)
} else {
None
}
}
fn tab_content<V: 'static>(
&self,
_: Option<usize>,

View File

@@ -771,7 +771,7 @@ impl CollabTitlebarItem {
})
.with_tooltip::<ToggleUserMenu>(
0,
"Toggle user menu".to_owned(),
"Toggle User Menu".to_owned(),
Some(Box::new(ToggleUserMenu)),
tooltip,
cx,

View File

@@ -555,67 +555,6 @@ impl DisplaySnapshot {
})
}
/// Returns an iterator of the start positions of the occurrences of `target` in the `self` after `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn find_while<'a>(
&'a self,
from: DisplayPoint,
target: &str,
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
Self::find_internal(self.chars_at(from), target.chars().collect(), condition)
}
/// Returns an iterator of the end positions of the occurrences of `target` in the `self` before `from`
/// Stops if `condition` returns false for any of the character position pairs observed.
pub fn reverse_find_while<'a>(
&'a self,
from: DisplayPoint,
target: &str,
condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
Self::find_internal(
self.reverse_chars_at(from),
target.chars().rev().collect(),
condition,
)
}
fn find_internal<'a>(
iterator: impl Iterator<Item = (char, DisplayPoint)> + 'a,
target: Vec<char>,
mut condition: impl FnMut(char, DisplayPoint) -> bool + 'a,
) -> impl Iterator<Item = DisplayPoint> + 'a {
// List of partial matches with the index of the last seen character in target and the starting point of the match
let mut partial_matches: Vec<(usize, DisplayPoint)> = Vec::new();
iterator
.take_while(move |(ch, point)| condition(*ch, *point))
.filter_map(move |(ch, point)| {
if Some(&ch) == target.get(0) {
partial_matches.push((0, point));
}
let mut found = None;
// Keep partial matches that have the correct next character
partial_matches.retain_mut(|(match_position, match_start)| {
if target.get(*match_position) == Some(&ch) {
*match_position += 1;
if *match_position == target.len() {
found = Some(match_start.clone());
// This match is completed. No need to keep tracking it
false
} else {
true
}
} else {
false
}
});
found
})
}
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
@@ -933,7 +872,7 @@ pub mod tests {
use smol::stream::StreamExt;
use std::{env, sync::Arc};
use theme::SyntaxTheme;
use util::test::{marked_text_offsets, marked_text_ranges, sample_text};
use util::test::{marked_text_ranges, sample_text};
use Bias::*;
#[gpui::test(iterations = 100)]
@@ -1744,32 +1683,6 @@ pub mod tests {
)
}
#[test]
fn test_find_internal() {
assert("This is a ˇtest of find internal", "test");
assert("Some text ˇaˇaˇaa with repeated characters", "aa");
fn assert(marked_text: &str, target: &str) {
let (text, expected_offsets) = marked_text_offsets(marked_text);
let chars = text
.chars()
.enumerate()
.map(|(index, ch)| (ch, DisplayPoint::new(0, index as u32)));
let target = target.chars();
assert_eq!(
expected_offsets
.into_iter()
.map(|offset| offset as u32)
.collect::<Vec<_>>(),
DisplaySnapshot::find_internal(chars, target.collect(), |_, _| true)
.map(|point| point.column())
.collect::<Vec<_>>()
)
}
}
fn syntax_chunks<'a>(
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,

View File

@@ -572,7 +572,7 @@ pub struct Editor {
project: Option<ModelHandle<Project>>,
focused: bool,
blink_manager: ModelHandle<BlinkManager>,
show_local_selections: bool,
pub show_local_selections: bool,
mode: EditorMode,
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
show_gutter: bool,
@@ -2269,10 +2269,6 @@ impl Editor {
if self.read_only {
return;
}
if !self.input_enabled {
cx.emit(Event::InputIgnored { text });
return;
}
let selections = self.selections.all_adjusted(cx);
let mut brace_inserted = false;
@@ -3207,17 +3203,30 @@ impl Editor {
.count();
let snapshot = self.buffer.read(cx).snapshot(cx);
let mut range_to_replace: Option<Range<isize>> = None;
let mut ranges = Vec::new();
for selection in &selections {
if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
let start = selection.start.saturating_sub(lookbehind);
let end = selection.end + lookahead;
if selection.id == newest_selection.id {
range_to_replace = Some(
((start + common_prefix_len) as isize - selection.start as isize)
..(end as isize - selection.start as isize),
);
}
ranges.push(start + common_prefix_len..end);
} else {
common_prefix_len = 0;
ranges.clear();
ranges.extend(selections.iter().map(|s| {
if s.id == newest_selection.id {
range_to_replace = Some(
old_range.start.to_offset_utf16(&snapshot).0 as isize
- selection.start as isize
..old_range.end.to_offset_utf16(&snapshot).0 as isize
- selection.start as isize,
);
old_range.clone()
} else {
s.start..s.end
@@ -3228,6 +3237,11 @@ impl Editor {
}
let text = &text[common_prefix_len..];
cx.emit(Event::InputHandled {
utf16_range_to_replace: range_to_replace,
text: text.into(),
});
self.transact(cx, |this, cx| {
if let Some(mut snippet) = snippet {
snippet.text = text.to_string();
@@ -3685,6 +3699,10 @@ impl Editor {
self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
}
cx.emit(Event::InputHandled {
utf16_range_to_replace: None,
text: suggestion.text.to_string().into(),
});
self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
cx.notify();
true
@@ -8436,6 +8454,41 @@ impl Editor {
pub fn inlay_hint_cache(&self) -> &InlayHintCache {
&self.inlay_hint_cache
}
pub fn replay_insert_event(
&mut self,
text: &str,
relative_utf16_range: Option<Range<isize>>,
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
cx.emit(Event::InputIgnored { text: text.into() });
return;
}
if let Some(relative_utf16_range) = relative_utf16_range {
let selections = self.selections.all::<OffsetUtf16>(cx);
self.change_selections(None, cx, |s| {
let new_ranges = selections.into_iter().map(|range| {
let start = OffsetUtf16(
range
.head()
.0
.saturating_add_signed(relative_utf16_range.start),
);
let end = OffsetUtf16(
range
.head()
.0
.saturating_add_signed(relative_utf16_range.end),
);
start..end
});
s.select_ranges(new_ranges);
});
}
self.handle_input(text, cx);
}
}
fn document_to_inlay_range(
@@ -8524,6 +8577,10 @@ pub enum Event {
InputIgnored {
text: Arc<str>,
},
InputHandled {
utf16_range_to_replace: Option<Range<isize>>,
text: Arc<str>,
},
ExcerptsAdded {
buffer: ModelHandle<Buffer>,
predecessor: ExcerptId,
@@ -8744,29 +8801,51 @@ impl View for Editor {
text: &str,
cx: &mut ViewContext<Self>,
) {
self.transact(cx, |this, cx| {
if this.input_enabled {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
if !self.input_enabled {
cx.emit(Event::InputIgnored { text: text.into() });
return;
}
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
self.transact(cx, |this, cx| {
let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
Some(this.selection_replacement_ranges(range_utf16, cx))
} else {
this.marked_text_ranges(cx)
};
let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
let newest_selection_id = this.selections.newest_anchor().id;
this.selections
.all::<OffsetUtf16>(cx)
.iter()
.zip(ranges_to_replace.iter())
.find_map(|(selection, range)| {
if selection.id == newest_selection_id {
Some(
(range.start.0 as isize - selection.head().0 as isize)
..(range.end.0 as isize - selection.head().0 as isize),
)
} else {
None
}
})
});
cx.emit(Event::InputHandled {
utf16_range_to_replace: range_to_replace,
text: text.into(),
});
if let Some(new_selected_ranges) = new_selected_ranges {
this.change_selections(None, cx, |selections| {
selections.select_ranges(new_selected_ranges)
});
}
this.handle_input(text, cx);
});
if !self.input_enabled {
return;
}
if let Some(transaction) = self.ime_transaction {
self.buffer.update(cx, |buffer, cx| {
buffer.group_until_transaction(transaction, cx);
@@ -8784,6 +8863,7 @@ impl View for Editor {
cx: &mut ViewContext<Self>,
) {
if !self.input_enabled {
cx.emit(Event::InputIgnored { text: text.into() });
return;
}
@@ -8808,6 +8888,29 @@ impl View for Editor {
None
};
let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
let newest_selection_id = this.selections.newest_anchor().id;
this.selections
.all::<OffsetUtf16>(cx)
.iter()
.zip(ranges_to_replace.iter())
.find_map(|(selection, range)| {
if selection.id == newest_selection_id {
Some(
(range.start.0 as isize - selection.head().0 as isize)
..(range.end.0 as isize - selection.head().0 as isize),
)
} else {
None
}
})
});
cx.emit(Event::InputHandled {
utf16_range_to_replace: range_to_replace,
text: text.into(),
});
if let Some(ranges) = ranges_to_replace {
this.change_selections(None, cx, |s| s.select_ranges(ranges));
}

View File

@@ -7807,7 +7807,7 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
/// Handle completion request passing a marked string specifying where the completion
/// should be triggered from using '|' character, what range should be replaced, and what completions
/// should be returned using '<' and '>' to delimit the range
fn handle_completion_request<'a>(
pub fn handle_completion_request<'a>(
cx: &mut EditorLspTestContext<'a>,
marked_string: &str,
completions: Vec<&'static str>,

View File

@@ -1528,8 +1528,13 @@ mod tests {
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
active_pane
.update(cx, |pane, cx| {
pane.close_active_item(&workspace::CloseActiveItem, cx)
.unwrap()
pane.close_active_item(
&workspace::CloseActiveItem {
save_behavior: None,
},
cx,
)
.unwrap()
})
.await
.unwrap();

View File

@@ -3513,14 +3513,12 @@ impl<'a, 'b, 'c, V> LayoutContext<'a, 'b, 'c, V> {
handler_depth = Some(contexts.len())
}
let action_contexts = if let Some(depth) = handler_depth {
&contexts[depth..]
} else {
&contexts
};
self.keystroke_matcher
.keystrokes_for_action(action, action_contexts)
let handler_depth = handler_depth.unwrap_or(0);
(0..=handler_depth).find_map(|depth| {
let contexts = &contexts[depth..];
self.keystroke_matcher
.keystrokes_for_action(action, contexts)
})
}
fn notify_if_view_ancestors_change(&mut self, view_id: usize) {
@@ -6499,7 +6497,7 @@ mod tests {
#[crate::test(self)]
fn test_keystrokes_for_action(cx: &mut TestAppContext) {
actions!(test, [Action1, Action2, GlobalAction]);
actions!(test, [Action1, Action2, Action3, GlobalAction]);
struct View1 {
child: ViewHandle<View2>,
@@ -6542,12 +6540,14 @@ mod tests {
cx.update(|cx| {
cx.add_action(|_: &mut View1, _: &Action1, _cx| {});
cx.add_action(|_: &mut View1, _: &Action3, _cx| {});
cx.add_action(|_: &mut View2, _: &Action2, _cx| {});
cx.add_global_action(|_: &GlobalAction, _| {});
cx.add_bindings(vec![
Binding::new("a", Action1, Some("View1")),
Binding::new("b", Action2, Some("View1 > View2")),
Binding::new("c", GlobalAction, Some("View3")), // View 3 does not exist
Binding::new("c", Action3, Some("View2")),
Binding::new("d", GlobalAction, Some("View3")), // View 3 does not exist
]);
});
@@ -6577,6 +6577,14 @@ mod tests {
.as_slice(),
&[Keystroke::parse("b").unwrap()]
);
assert_eq!(layout_cx.keystrokes_for_action(view_1.id(), &Action3), None);
assert_eq!(
layout_cx
.keystrokes_for_action(view_2.id(), &Action3)
.unwrap()
.as_slice(),
&[Keystroke::parse("c").unwrap()]
);
// The 'a' keystroke propagates up the view tree from view_2
// to view_1. The action, Action1, is handled by view_1.
@@ -6604,7 +6612,8 @@ mod tests {
&available_actions(window.into(), view_1.id(), cx),
&[
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
("test::GlobalAction", vec![])
("test::Action3", vec![]),
("test::GlobalAction", vec![]),
],
);
@@ -6614,6 +6623,7 @@ mod tests {
&[
("test::Action1", vec![Keystroke::parse("a").unwrap()]),
("test::Action2", vec![Keystroke::parse("b").unwrap()]),
("test::Action3", vec![Keystroke::parse("c").unwrap()]),
("test::GlobalAction", vec![]),
],
);

View File

@@ -1110,7 +1110,7 @@ impl<'a> WindowContext<'a> {
self.window.is_fullscreen
}
pub(crate) fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
pub fn dispatch_action(&mut self, view_id: Option<usize>, action: &dyn Action) -> bool {
if let Some(view_id) = view_id {
self.halt_action_dispatch = false;
self.visit_dispatch_path(view_id, |view_id, capture_phase, cx| {

View File

@@ -52,6 +52,7 @@ impl View for ActiveBufferLanguage {
} else {
"Unknown".to_string()
};
let theme = theme::current(cx).clone();
MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
let theme = &theme::current(cx).workspace.status_bar;
@@ -68,6 +69,7 @@ impl View for ActiveBufferLanguage {
});
}
})
.with_tooltip::<Self>(0, "Select Language", None, theme.tooltip.clone(), cx)
.into_any()
} else {
Empty::new().into_any()

View File

@@ -41,9 +41,6 @@ schemars.workspace = true
globset.workspace = true
sha1 = "0.10.5"
parse_duration = "2.1.1"
ort = { version = "1.15.2", features = ["coreml"]}
tokenizers = { version = ">=0.13.4", default-features = false, features = [ "onig" ] }
ndarray = "0.15"
[dev-dependencies]
collections = { path = "../collections", features = ["test-support"] }

View File

@@ -1,115 +0,0 @@
use ndarray::CowArray;
use ort::{Environment, ExecutionProvider, GraphOptimizationLevel, Session, SessionBuilder, Value};
use tokenizers::Tokenizer;
use util::paths::MODELS_DIR;
pub struct CrossEncoder {
session: Session,
tokenizer: Tokenizer,
}
fn sigmoid(val: f32) -> f32 {
1.0 / (1.0 + (-val).exp())
}
impl CrossEncoder {
pub fn load() -> anyhow::Result<Self> {
let model_path = MODELS_DIR.join("cross-encoder").join("model.onnx");
let tokenizer_path = MODELS_DIR.join("cross-encoder").join("tokenizer.json");
let environment = Environment::builder()
.with_name("cross-encoder")
.with_execution_providers([ExecutionProvider::CoreML(Default::default())])
.build()?
.into_arc();
let session = SessionBuilder::new(&environment)?
.with_optimization_level(GraphOptimizationLevel::Level1)?
.with_model_from_file(model_path)?;
let mut tokenizer = Tokenizer::from_file(tokenizer_path).unwrap();
tokenizer
.with_truncation(Some(tokenizers::TruncationParams {
direction: Default::default(),
max_length: 512,
strategy: Default::default(),
stride: 0,
}))
.unwrap();
Ok(Self { session, tokenizer })
}
pub fn score(&self, query: &str, candidates: &[String]) -> anyhow::Result<Vec<f32>> {
let spans = candidates
.into_iter()
.map(|candidate| format!("{}. {}", query, candidate))
.collect::<Vec<_>>();
let encodings = self.tokenizer.encode_batch(spans, true).unwrap();
let mut results = Vec::new();
for encoding in encodings {
// Get Input Variables Individually
let input_ids = encoding.get_ids();
let attention_mask = encoding.get_attention_mask();
let token_type_ids = encoding.get_type_ids();
let length = input_ids.len();
// Convert to Arrays
let inputs_ids_array = CowArray::from(ndarray::Array::from_shape_vec(
(1, length),
input_ids.iter().map(|&x| x as i64).collect(),
)?);
let attention_mask_array = CowArray::from(ndarray::Array::from_shape_vec(
(1, length),
attention_mask.iter().map(|&x| x as i64).collect(),
)?)
.into_dyn();
let token_type_ids_array = CowArray::from(ndarray::Array::from_shape_vec(
(1, length),
token_type_ids.iter().map(|&x| x as i64).collect(),
)?)
.into_dyn();
let outputs = self.session.run(vec![
Value::from_array(self.session.allocator(), &inputs_ids_array.into_dyn())?,
Value::from_array(self.session.allocator(), &attention_mask_array)?,
Value::from_array(self.session.allocator(), &token_type_ids_array)?,
]);
let output = outputs.unwrap()[0].try_extract::<f32>().unwrap();
let value = output.view().to_owned();
let val = value.as_slice().unwrap()[0];
results.push(sigmoid(val))
}
Ok(results)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cross_encoder() {
let cross_encoder = CrossEncoder::load().unwrap();
let results = cross_encoder
.score(
"I like you",
&[
"I hate you.".into(),
"I love you.".into(),
"my name is kyle".into(),
],
)
.unwrap();
assert_eq!(results.len(), 3);
assert!(results[1] > results[0]);
assert!(results[0] > results[2]);
}
}

View File

@@ -1,4 +1,3 @@
mod cross_encoder;
mod db;
mod embedding;
mod embedding_queue;
@@ -8,7 +7,7 @@ pub mod semantic_index_settings;
#[cfg(test)]
mod semantic_index_tests;
use crate::{cross_encoder::CrossEncoder, semantic_index_settings::SemanticIndexSettings};
use crate::semantic_index_settings::SemanticIndexSettings;
use anyhow::{anyhow, Result};
use collections::{BTreeMap, HashMap, HashSet};
use db::VectorDatabase;
@@ -266,7 +265,6 @@ pub struct PendingFile {
pub struct SearchResult {
pub buffer: ModelHandle<Buffer>,
pub range: Range<Anchor>,
pub similarity: f32,
}
impl SemanticIndex {
@@ -698,7 +696,7 @@ impl SemanticIndex {
let embedding_provider = self.embedding_provider.clone();
let db_path = self.db.path().clone();
let fs = self.fs.clone();
cx.spawn(|this, cx| async move {
cx.spawn(|this, mut cx| async move {
index.await?;
let t0 = Instant::now();
@@ -710,7 +708,7 @@ impl SemanticIndex {
}
let phrase_embedding = embedding_provider
.embed_batch(vec![phrase.clone()])
.embed_batch(vec![phrase])
.await?
.into_iter()
.next()
@@ -751,11 +749,6 @@ impl SemanticIndex {
ids_len / batch_n
};
let cross_encoder = Arc::new(
cx.background()
.spawn(async move { CrossEncoder::load() })
.await?,
);
let mut batch_results = Vec::new();
for batch in file_ids.chunks(batch_size) {
let batch = batch.into_iter().map(|v| *v).collect::<Vec<i64>>();
@@ -763,126 +756,77 @@ impl SemanticIndex {
let fs = fs.clone();
let db_path = db_path.clone();
let phrase_embedding = phrase_embedding.clone();
let phrase = phrase.clone();
let cross_encoder = cross_encoder.clone();
let project = project.clone();
if let Some(db) = VectorDatabase::new(fs, db_path.clone(), cx.background())
.await
.log_err()
{
let this = this.clone();
batch_results.push(cx.spawn(|mut cx| async move {
let span_ids = db
.top_k_search(&phrase_embedding, limit, batch.as_slice())
.await?
.into_iter()
.map(|(span_id, _)| span_id)
.collect::<Vec<_>>();
let mut spans_by_buffer = HashMap::default();
for (worktree_db_id, path, range) in db.spans_for_ids(&span_ids).await? {
let worktree_id = this.read_with(&cx, |this, _| {
let project_state = this
.projects
.get(&project.downgrade())
.ok_or_else(|| anyhow!("project not added"))?;
anyhow::Ok(project_state.worktree_id_for_db_id(worktree_db_id))
})?;
if let Some(worktree_id) = worktree_id {
let buffer = project
.update(&mut cx, |project, cx| {
project.open_buffer((worktree_id, path), cx)
})
.await
.log_err();
if let Some(buffer) = buffer {
let range = buffer.read_with(&cx, |buffer, _| {
let range = buffer.clip_offset(range.start, Bias::Left)
..buffer.clip_offset(range.end, Bias::Right);
buffer.anchor_before(range.start)
..buffer.anchor_after(range.end)
});
spans_by_buffer
.entry(buffer)
.or_insert(Vec::new())
.push(range);
}
}
}
let mut spans = Vec::new();
for (buffer, ranges) in &spans_by_buffer {
buffer.read_with(&cx, |buffer, _| {
for range in ranges {
let span =
buffer.text_for_range(range.clone()).collect::<String>();
spans.push(span);
}
});
}
// Cross Encoder
// TODO: move background.spawn into cross_encoder.
let results = cx
.background()
.spawn(async move {
let mut results = Vec::new();
let mut scores = cross_encoder.score(&phrase, &spans)?.into_iter();
for (buffer, ranges) in spans_by_buffer {
for range in ranges {
let similarity = if let Some(similarity) = scores.next() {
similarity
} else {
log::error!("cross encoder returned too few scores");
f32::NEG_INFINITY
};
results.push(SearchResult {
buffer: buffer.clone(),
range,
similarity,
});
}
}
anyhow::Ok(results)
})
.await?;
anyhow::Ok(results)
}));
batch_results.push(async move {
db.top_k_search(&phrase_embedding, limit, batch.as_slice())
.await
});
}
}
let batch_results = futures::future::join_all(batch_results).await;
let mut results = Vec::<SearchResult>::new();
let mut results = Vec::new();
for batch_result in batch_results {
if let Some(batch_result) = batch_result.log_err() {
for new_result in batch_result {
let ix = match results.binary_search_by(|old_result| {
new_result
.similarity
.partial_cmp(&old_result.similarity)
.unwrap_or(Ordering::Equal)
if batch_result.is_ok() {
for (id, similarity) in batch_result.unwrap() {
let ix = match results.binary_search_by(|(_, s)| {
similarity.partial_cmp(&s).unwrap_or(Ordering::Equal)
}) {
Ok(ix) => ix,
Err(ix) => ix,
};
dbg!(ix);
dbg!(new_result.similarity);
results.insert(ix, new_result);
results.insert(ix, (id, similarity));
results.truncate(limit);
}
}
}
let ids = results.into_iter().map(|(id, _)| id).collect::<Vec<i64>>();
let spans = database.spans_for_ids(ids.as_slice()).await?;
let mut tasks = Vec::new();
let mut ranges = Vec::new();
let weak_project = project.downgrade();
project.update(&mut cx, |project, cx| {
for (worktree_db_id, file_path, byte_range) in spans {
let project_state =
if let Some(state) = this.read(cx).projects.get(&weak_project) {
state
} else {
return Err(anyhow!("project not added"));
};
if let Some(worktree_id) = project_state.worktree_id_for_db_id(worktree_db_id) {
tasks.push(project.open_buffer((worktree_id, file_path), cx));
ranges.push(byte_range);
}
}
Ok(())
})?;
let buffers = futures::future::join_all(tasks).await;
log::trace!(
"Semantic Searching took: {:?} milliseconds in total",
t0.elapsed().as_millis()
);
Ok(results)
Ok(buffers
.into_iter()
.zip(ranges)
.filter_map(|(buffer, range)| {
let buffer = buffer.log_err()?;
let range = buffer.read_with(&cx, |buffer, _| {
let start = buffer.clip_offset(range.start, Bias::Left);
let end = buffer.clip_offset(range.end, Bias::Right);
buffer.anchor_before(start)..buffer.anchor_after(end)
});
Some(SearchResult { buffer, range })
})
.collect::<Vec<_>>())
})
}

View File

@@ -2,13 +2,13 @@ Design notes:
This crate is split into two conceptual halves:
- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here.
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file.
ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context.
The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors.
#Input
#Input
There are currently many distinct paths for getting keystrokes to the terminal:
@@ -18,6 +18,6 @@ There are currently many distinct paths for getting keystrokes to the terminal:
3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`.
4. Pasted text has a separate pathway.
4. Pasted text has a separate pathway.
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal
Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal

View File

@@ -283,7 +283,12 @@ impl TerminalView {
pub fn deploy_context_menu(&mut self, position: Vector2F, cx: &mut ViewContext<Self>) {
let menu_entries = vec![
ContextMenuItem::action("Clear", Clear),
ContextMenuItem::action("Close", pane::CloseActiveItem),
ContextMenuItem::action(
"Close",
pane::CloseActiveItem {
save_behavior: None,
},
),
];
self.context_menu.update(cx, |menu, cx| {

View File

@@ -18,7 +18,6 @@ lazy_static::lazy_static! {
pub static ref LOG: PathBuf = LOGS_DIR.join("Zed.log");
pub static ref OLD_LOG: PathBuf = LOGS_DIR.join("Zed.log.old");
pub static ref LOCAL_SETTINGS_RELATIVE_PATH: &'static Path = Path::new(".zed/settings.json");
pub static ref MODELS_DIR: PathBuf = HOME.join("Library/Application Support/Zed/models");
}
pub mod legacy {

View File

@@ -38,6 +38,7 @@ language_selector = { path = "../language_selector"}
[dev-dependencies]
indoc.workspace = true
parking_lot.workspace = true
futures.workspace = true
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
@@ -47,3 +48,4 @@ util = { path = "../util", features = ["test-support"] }
settings = { path = "../settings" }
workspace = { path = "../workspace", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }

View File

@@ -34,6 +34,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
editor.window().update(cx, |cx| {
Vim::update(cx, |vim, cx| {
vim.workspace_state.recording = false;
if let Some(previous_editor) = vim.active_editor.clone() {
if previous_editor == editor.clone() {
vim.active_editor = None;

View File

@@ -11,8 +11,9 @@ pub fn init(cx: &mut AppContext) {
}
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |state, cx| {
state.update_active_editor(cx, |editor, cx| {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
@@ -20,7 +21,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
});
});
});
state.switch_mode(Mode::Normal, false, cx);
vim.switch_mode(Mode::Normal, false, cx);
})
}

View File

@@ -1,9 +1,9 @@
use std::{cmp, sync::Arc};
use std::cmp;
use editor::{
char_kind,
display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
movement::{self, FindRange},
movement::{self, find_boundary, find_preceding_boundary, FindRange},
Bias, CharKind, DisplayPoint, ToOffset,
};
use gpui::{actions, impl_actions, AppContext, WindowContext};
@@ -37,8 +37,8 @@ pub enum Motion {
StartOfDocument,
EndOfDocument,
Matching,
FindForward { before: bool, text: Arc<str> },
FindBackward { after: bool, text: Arc<str> },
FindForward { before: bool, char: char },
FindBackward { after: bool, char: char },
NextLineStart,
}
@@ -65,9 +65,9 @@ struct PreviousWordStart {
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Up {
pub(crate) struct Up {
#[serde(default)]
display_lines: bool,
pub(crate) display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -93,9 +93,9 @@ struct EndOfLine {
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct StartOfLine {
pub struct StartOfLine {
#[serde(default)]
display_lines: bool,
pub(crate) display_lines: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
@@ -233,25 +233,25 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
let find = match Vim::read(cx).workspace_state.last_find.clone() {
Some(Motion::FindForward { before, text }) => {
Some(Motion::FindForward { before, char }) => {
if backwards {
Motion::FindBackward {
after: before,
text,
char,
}
} else {
Motion::FindForward { before, text }
Motion::FindForward { before, char }
}
}
Some(Motion::FindBackward { after, text }) => {
Some(Motion::FindBackward { after, char }) => {
if backwards {
Motion::FindForward {
before: after,
text,
char,
}
} else {
Motion::FindBackward { after, text }
Motion::FindBackward { after, char }
}
}
_ => return,
@@ -403,12 +403,12 @@ impl Motion {
SelectionGoal::None,
),
Matching => (matching(map, point), SelectionGoal::None),
FindForward { before, text } => (
find_forward(map, point, *before, text.clone(), times),
FindForward { before, char } => (
find_forward(map, point, *before, *char, times),
SelectionGoal::None,
),
FindBackward { after, text } => (
find_backward(map, point, *after, text.clone(), times),
FindBackward { after, char } => (
find_backward(map, point, *after, *char, times),
SelectionGoal::None,
),
NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
@@ -793,44 +793,55 @@ fn find_forward(
map: &DisplaySnapshot,
from: DisplayPoint,
before: bool,
target: Arc<str>,
target: char,
times: usize,
) -> DisplayPoint {
map.find_while(from, target.as_ref(), |ch, _| ch != '\n')
.skip_while(|found_at| found_at == &from)
.nth(times - 1)
.map(|mut found| {
if before {
*found.column_mut() -= 1;
found = map.clip_point(found, Bias::Right);
found
} else {
found
}
})
.unwrap_or(from)
let mut to = from;
let mut found = false;
for _ in 0..times {
found = false;
to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
found = right == target;
found
});
}
if found {
if before && to.column() > 0 {
*to.column_mut() -= 1;
map.clip_point(to, Bias::Left)
} else {
to
}
} else {
from
}
}
fn find_backward(
map: &DisplaySnapshot,
from: DisplayPoint,
after: bool,
target: Arc<str>,
target: char,
times: usize,
) -> DisplayPoint {
map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n')
.skip_while(|found_at| found_at == &from)
.nth(times - 1)
.map(|mut found| {
if after {
*found.column_mut() += 1;
found = map.clip_point(found, Bias::Left);
found
} else {
found
}
})
.unwrap_or(from)
let mut to = from;
for _ in 0..times {
to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
}
if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
if after {
*to.column_mut() += 1;
map.clip_point(to, Bias::Right)
} else {
to
}
} else {
from
}
}
fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {

View File

@@ -2,6 +2,7 @@ mod case;
mod change;
mod delete;
mod paste;
mod repeat;
mod scroll;
mod search;
pub mod substitute;
@@ -34,6 +35,7 @@ actions!(
vim,
[
InsertAfter,
InsertBefore,
InsertFirstNonWhitespace,
InsertEndOfLine,
InsertLineAbove,
@@ -44,32 +46,42 @@ actions!(
DeleteToEndOfLine,
Yank,
ChangeCase,
JoinLines,
]
);
pub fn init(cx: &mut AppContext) {
paste::init(cx);
repeat::init(cx);
scroll::init(cx);
search::init(cx);
substitute::init(cx);
cx.add_action(insert_after);
cx.add_action(insert_before);
cx.add_action(insert_first_non_whitespace);
cx.add_action(insert_end_of_line);
cx.add_action(insert_line_above);
cx.add_action(insert_line_below);
cx.add_action(change_case);
substitute::init(cx);
search::init(cx);
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let times = vim.pop_number_operator(cx);
delete_motion(vim, Motion::Left, times, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let times = vim.pop_number_operator(cx);
delete_motion(vim, Motion::Right, times, cx);
})
});
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
let times = vim.pop_number_operator(cx);
change_motion(
vim,
@@ -83,6 +95,7 @@ pub fn init(cx: &mut AppContext) {
});
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let times = vim.pop_number_operator(cx);
delete_motion(
vim,
@@ -94,8 +107,26 @@ pub fn init(cx: &mut AppContext) {
);
})
});
scroll::init(cx);
paste::init(cx);
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let mut times = vim.pop_number_operator(cx).unwrap_or(1);
if vim.state().mode.is_visual() {
times = 1;
} else if times > 1 {
// 2J joins two lines together (same as J or 1J)
times -= 1;
}
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
for _ in 0..times {
editor.join_lines(&Default::default(), cx)
}
})
})
})
})
}
pub fn normal_motion(
@@ -151,6 +182,7 @@ fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut Win
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -162,12 +194,20 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
});
}
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
});
}
fn insert_first_non_whitespace(
_: &mut Workspace,
_: &InsertFirstNonWhitespace,
cx: &mut ViewContext<Workspace>,
) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -184,6 +224,7 @@ fn insert_first_non_whitespace(
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
@@ -197,6 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
@@ -229,6 +271,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
vim.switch_mode(Mode::Insert, false, cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
@@ -260,6 +303,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -780,6 +824,7 @@ mod test {
#[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

View File

@@ -7,6 +7,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
vim.update_active_editor(cx, |editor, cx| {
let mut ranges = Vec::new();
@@ -21,10 +22,16 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
ranges.push(start..end);
cursor_positions.push(start..start);
}
Mode::Visual | Mode::VisualBlock => {
Mode::Visual => {
ranges.push(selection.start..selection.end);
cursor_positions.push(selection.start..selection.start);
}
Mode::VisualBlock => {
ranges.push(selection.start..selection.end);
if cursor_positions.len() == 0 {
cursor_positions.push(selection.start..selection.start);
}
}
Mode::Insert | Mode::Normal => {
let start = selection.start;
let mut end = start;
@@ -96,6 +103,11 @@ mod test {
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
cx.assert_shared_state("ˇABc\n").await;
// works in visual block mode
cx.set_shared_state("ˇaa\nbb\ncc").await;
cx.simulate_shared_keystrokes(["ctrl-v", "j", "~"]).await;
cx.assert_shared_state("ˇAa\nBb\ncc").await;
// works with multiple cursors (zed only)
cx.set_state("aˇßcdˇe\n", Mode::Normal);
cx.simulate_keystroke("~");

View File

@@ -4,6 +4,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
use gpui::WindowContext;
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
@@ -37,6 +38,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
}
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);

View File

@@ -28,6 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) {
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);

View File

@@ -0,0 +1,427 @@
use crate::{
motion::Motion,
state::{Mode, RecordedSelection, ReplayableAction},
visual::visual_motion,
Vim,
};
use gpui::{actions, Action, AppContext};
use workspace::Workspace;
actions!(vim, [Repeat, EndRepeat,]);
fn should_replay(action: &Box<dyn Action>) -> bool {
// skip so that we don't leave the character palette open
if editor::ShowCharacterPalette.id() == action.id() {
return false;
}
true
}
pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
Vim::update(cx, |vim, cx| {
vim.workspace_state.replaying = false;
vim.update_active_editor(cx, |editor, _| {
editor.show_local_selections = true;
});
vim.switch_mode(Mode::Normal, false, cx)
});
});
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
let actions = vim.workspace_state.recorded_actions.clone();
let Some(editor) = vim.active_editor.clone() else {
return None;
};
let count = vim.pop_number_operator(cx);
vim.workspace_state.replaying = true;
let selection = vim.workspace_state.recorded_selection.clone();
match selection {
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
vim.workspace_state.recorded_count = None;
vim.switch_mode(Mode::Visual, false, cx)
}
RecordedSelection::VisualLine { .. } => {
vim.workspace_state.recorded_count = None;
vim.switch_mode(Mode::VisualLine, false, cx)
}
RecordedSelection::VisualBlock { .. } => {
vim.workspace_state.recorded_count = None;
vim.switch_mode(Mode::VisualBlock, false, cx)
}
RecordedSelection::None => {
if let Some(count) = count {
vim.workspace_state.recorded_count = Some(count);
}
}
}
if let Some(editor) = editor.upgrade(cx) {
editor.update(cx, |editor, _| {
editor.show_local_selections = false;
})
} else {
return None;
}
Some((actions, editor, selection))
}) else {
return;
};
match selection {
RecordedSelection::SingleLine { cols } => {
if cols > 1 {
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
}
}
RecordedSelection::Visual { rows, cols } => {
visual_motion(
Motion::Down {
display_lines: false,
},
Some(rows as usize),
cx,
);
visual_motion(
Motion::StartOfLine {
display_lines: false,
},
None,
cx,
);
if cols > 1 {
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
}
}
RecordedSelection::VisualBlock { rows, cols } => {
visual_motion(
Motion::Down {
display_lines: false,
},
Some(rows as usize),
cx,
);
if cols > 1 {
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
}
}
RecordedSelection::VisualLine { rows } => {
visual_motion(
Motion::Down {
display_lines: false,
},
Some(rows as usize),
cx,
);
}
RecordedSelection::None => {}
}
let window = cx.window();
cx.app_context()
.spawn(move |mut cx| async move {
for action in actions {
match action {
ReplayableAction::Action(action) => {
if should_replay(&action) {
window
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
.ok_or_else(|| anyhow::anyhow!("window was closed"))
} else {
Ok(())
}
}
ReplayableAction::Insertion {
text,
utf16_range_to_replace,
} => editor.update(&mut cx, |editor, cx| {
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
}),
}?
}
window
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
.ok_or_else(|| anyhow::anyhow!("window was closed"))
})
.detach_and_log_err(cx);
});
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use editor::test::editor_lsp_test_context::EditorLspTestContext;
use futures::StreamExt;
use indoc::indoc;
use gpui::{executor::Deterministic, View};
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
#[gpui::test]
async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// "o"
cx.set_shared_state("ˇhello").await;
cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
.await;
cx.assert_shared_state("hello\nworlˇd").await;
cx.simulate_shared_keystrokes(["."]).await;
deterministic.run_until_parked();
cx.assert_shared_state("hello\nworld\nworlˇd").await;
// "d"
cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
cx.simulate_shared_keystrokes(["g", "g", "."]).await;
deterministic.run_until_parked();
cx.assert_shared_state("ˇ\nworld\nrld").await;
// "p" (note that it pastes the current clipboard)
cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
.await;
deterministic.run_until_parked();
cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
// "~" (note that counts apply to the action taken, not . itself)
cx.set_shared_state("ˇthe quick brown fox").await;
cx.simulate_shared_keystrokes(["2", "~", "."]).await;
deterministic.run_until_parked();
cx.set_shared_state("THE ˇquick brown fox").await;
cx.simulate_shared_keystrokes(["3", "."]).await;
deterministic.run_until_parked();
cx.set_shared_state("THE QUIˇck brown fox").await;
deterministic.run_until_parked();
cx.simulate_shared_keystrokes(["."]).await;
deterministic.run_until_parked();
cx.set_shared_state("THE QUICK ˇbrown fox").await;
}
#[gpui::test]
async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state("hˇllo", Mode::Normal);
cx.simulate_keystrokes(["i"]);
// simulate brazilian input for ä.
cx.update_editor(|editor, cx| {
editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
editor.replace_text_in_range(None, "ä", cx);
});
cx.simulate_keystrokes(["escape"]);
cx.assert_state("hˇällo", Mode::Normal);
cx.simulate_keystrokes(["."]);
deterministic.run_until_parked();
cx.assert_state("hˇäällo", Mode::Normal);
}
#[gpui::test]
async fn test_repeat_completion(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
let cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
resolve_provider: Some(true),
..Default::default()
}),
..Default::default()
},
cx,
)
.await;
let mut cx = VimTestContext::new_with_lsp(cx, true);
cx.set_state(
indoc! {"
onˇe
two
three
"},
Mode::Normal,
);
let mut request =
cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
let position = params.text_document_position.position;
Ok(Some(lsp::CompletionResponse::Array(vec![
lsp::CompletionItem {
label: "first".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(position.clone(), position.clone()),
new_text: "first".to_string(),
})),
..Default::default()
},
lsp::CompletionItem {
label: "second".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range::new(position.clone(), position.clone()),
new_text: "second".to_string(),
})),
..Default::default()
},
])))
});
cx.simulate_keystrokes(["a", "."]);
request.next().await;
cx.condition(|editor, _| editor.context_menu_visible())
.await;
cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
cx.assert_state(
indoc! {"
one.secondˇ!
two
three
"},
Mode::Normal,
);
cx.simulate_keystrokes(["j", "."]);
deterministic.run_until_parked();
cx.assert_state(
indoc! {"
one.second!
two.secondˇ!
three
"},
Mode::Normal,
);
}
#[gpui::test]
async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
// single-line (3 columns)
cx.set_shared_state(indoc! {
"ˇthe quick brown
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
.await;
cx.assert_shared_state(indoc! {
"ˇo quick brown
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["j", "w", "."]).await;
deterministic.run_until_parked();
cx.assert_shared_state(indoc! {
"o quick brown
fox ˇops over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["f", "r", "."]).await;
deterministic.run_until_parked();
cx.assert_shared_state(indoc! {
"o quick brown
fox ops oveˇothe lazy dog"
})
.await;
// visual
cx.set_shared_state(indoc! {
"the ˇquick brown
fox jumps over
fox jumps over
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
cx.assert_shared_state(indoc! {
"the ˇumps over
fox jumps over
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["."]).await;
deterministic.run_until_parked();
cx.assert_shared_state(indoc! {
"the ˇumps over
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["w", "."]).await;
deterministic.run_until_parked();
cx.assert_shared_state(indoc! {
"the umps ˇumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["j", "."]).await;
deterministic.run_until_parked();
cx.assert_shared_state(indoc! {
"the umps umps over
the ˇog"
})
.await;
// block mode (3 rows)
cx.set_shared_state(indoc! {
"ˇthe quick brown
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
.await;
cx.assert_shared_state(indoc! {
"ˇothe quick brown
ofox jumps over
othe lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
deterministic.run_until_parked();
cx.assert_shared_state(indoc! {
"othe quick brown
ofoxˇo jumps over
otheo lazy dog"
})
.await;
// line mode
cx.set_shared_state(indoc! {
"ˇthe quick brown
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
.await;
cx.assert_shared_state(indoc! {
"ˇo
fox jumps over
the lazy dog"
})
.await;
cx.simulate_shared_keystrokes(["j", "."]).await;
deterministic.run_until_parked();
cx.assert_shared_state(indoc! {
"o
ˇo
the lazy dog"
})
.await;
}
}

View File

@@ -10,6 +10,7 @@ actions!(vim, [Substitute, SubstituteLine]);
pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
let count = vim.pop_number_operator(cx);
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
})
@@ -17,6 +18,7 @@ pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
Vim::update(cx, |vim, cx| {
vim.start_recording(cx);
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
vim.switch_mode(Mode::VisualLine, false, cx)
}

View File

@@ -1,4 +1,6 @@
use gpui::keymap_matcher::KeymapContext;
use std::{ops::Range, sync::Arc};
use gpui::{keymap_matcher::KeymapContext, Action};
use language::CursorShape;
use serde::{Deserialize, Serialize};
use workspace::searchable::Direction;
@@ -48,10 +50,61 @@ pub struct EditorState {
pub operator_stack: Vec<Operator>,
}
#[derive(Default, Clone, Debug)]
pub enum RecordedSelection {
#[default]
None,
Visual {
rows: u32,
cols: u32,
},
SingleLine {
cols: u32,
},
VisualBlock {
rows: u32,
cols: u32,
},
VisualLine {
rows: u32,
},
}
#[derive(Default, Clone)]
pub struct WorkspaceState {
pub search: SearchState,
pub last_find: Option<Motion>,
pub recording: bool,
pub stop_recording_after_next_action: bool,
pub replaying: bool,
pub recorded_count: Option<usize>,
pub recorded_actions: Vec<ReplayableAction>,
pub recorded_selection: RecordedSelection,
}
#[derive(Debug)]
pub enum ReplayableAction {
Action(Box<dyn Action>),
Insertion {
text: Arc<str>,
utf16_range_to_replace: Option<Range<isize>>,
},
}
impl Clone for ReplayableAction {
fn clone(&self) -> Self {
match self {
Self::Action(action) => Self::Action(action.boxed_clone()),
Self::Insertion {
text,
utf16_range_to_replace,
} => Self::Insertion {
text: text.clone(),
utf16_range_to_replace: utf16_range_to_replace.clone(),
},
}
}
}
#[derive(Clone)]

View File

@@ -286,6 +286,55 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) {
)
}
#[gpui::test]
async fn test_join_lines(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state(indoc! {"
ˇone
two
three
four
five
six
"})
.await;
cx.simulate_shared_keystrokes(["shift-j"]).await;
cx.assert_shared_state(indoc! {"
oneˇ two
three
four
five
six
"})
.await;
cx.simulate_shared_keystrokes(["3", "shift-j"]).await;
cx.assert_shared_state(indoc! {"
one two threeˇ four
five
six
"})
.await;
cx.set_shared_state(indoc! {"
ˇone
two
three
four
five
six
"})
.await;
cx.simulate_shared_keystrokes(["j", "v", "3", "j", "shift-j"])
.await;
cx.assert_shared_state(indoc! {"
one
two three fourˇ five
six
"})
.await;
}
#[gpui::test]
async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
@@ -449,6 +498,13 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
fourteen char
"})
.await;
cx.simulate_shared_keystrokes(["j", "shift-f", "e", "f", "r"])
.await;
cx.assert_shared_state(indoc! {"
fourteen•
fourteen chaˇr
"})
.await;
}
#[gpui::test]

View File

@@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut};
use editor::test::{
editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
};
use futures::Future;
use gpui::ContextHandle;
use lsp::request;
use search::{BufferSearchBar, ProjectSearchBar};
use crate::{state::Operator, *};
@@ -124,6 +126,19 @@ impl<'a> VimTestContext<'a> {
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
}
pub fn handle_request<T, F, Fut>(
&self,
handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
self.cx.handle_request::<T, F, Fut>(handler)
}
}
impl<'a> Deref for VimTestContext<'a> {

View File

@@ -18,17 +18,19 @@ use gpui::{
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use language::{CursorShape, Selection, SelectionGoal};
use language::{CursorShape, Point, Selection, SelectionGoal};
pub use mode_indicator::ModeIndicator;
use motion::Motion;
use normal::normal_replace;
use serde::Deserialize;
use settings::{Setting, SettingsStore};
use state::{EditorState, Mode, Operator, WorkspaceState};
use std::sync::Arc;
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
use std::{ops::Range, sync::Arc};
use visual::{visual_block_motion, visual_replace};
use workspace::{self, Workspace};
use crate::state::ReplayableAction;
struct VimModeSetting(bool);
#[derive(Clone, Deserialize, PartialEq)]
@@ -102,6 +104,19 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
return true;
}
if let Some(handled_by) = handled_by {
Vim::update(cx, |vim, _| {
if vim.workspace_state.recording {
vim.workspace_state
.recorded_actions
.push(ReplayableAction::Action(handled_by.boxed_clone()));
if vim.workspace_state.stop_recording_after_next_action {
vim.workspace_state.recording = false;
vim.workspace_state.stop_recording_after_next_action = false;
}
}
});
// Keystroke is handled by the vim system, so continue forward
if handled_by.namespace() == "vim" {
return true;
@@ -156,7 +171,12 @@ impl Vim {
}
Event::InputIgnored { text } => {
Vim::active_editor_input_ignored(text.clone(), cx);
Vim::record_insertion(text, None, cx)
}
Event::InputHandled {
text,
utf16_range_to_replace: range_to_replace,
} => Vim::record_insertion(text, range_to_replace.clone(), cx),
_ => {}
}));
@@ -176,6 +196,27 @@ impl Vim {
self.sync_vim_settings(cx);
}
fn record_insertion(
text: &Arc<str>,
range_to_replace: Option<Range<isize>>,
cx: &mut WindowContext,
) {
Vim::update(cx, |vim, _| {
if vim.workspace_state.recording {
vim.workspace_state
.recorded_actions
.push(ReplayableAction::Insertion {
text: text.clone(),
utf16_range_to_replace: range_to_replace,
});
if vim.workspace_state.stop_recording_after_next_action {
vim.workspace_state.recording = false;
vim.workspace_state.stop_recording_after_next_action = false;
}
}
});
}
fn update_active_editor<S>(
&self,
cx: &mut WindowContext,
@@ -184,6 +225,71 @@ impl Vim {
let editor = self.active_editor.clone()?.upgrade(cx)?;
Some(editor.update(cx, update))
}
// ~, shift-j, x, shift-x, p
// shift-c, shift-d, shift-i, i, a, o, shift-o, s
// c, d
// r
// TODO: shift-j?
//
pub fn start_recording(&mut self, cx: &mut WindowContext) {
if !self.workspace_state.replaying {
self.workspace_state.recording = true;
self.workspace_state.recorded_actions = Default::default();
self.workspace_state.recorded_count =
if let Some(Operator::Number(number)) = self.active_operator() {
Some(number)
} else {
None
};
let selections = self
.active_editor
.and_then(|editor| editor.upgrade(cx))
.map(|editor| {
let editor = editor.read(cx);
(
editor.selections.oldest::<Point>(cx),
editor.selections.newest::<Point>(cx),
)
});
if let Some((oldest, newest)) = selections {
self.workspace_state.recorded_selection = match self.state().mode {
Mode::Visual if newest.end.row == newest.start.row => {
RecordedSelection::SingleLine {
cols: newest.end.column - newest.start.column,
}
}
Mode::Visual => RecordedSelection::Visual {
rows: newest.end.row - newest.start.row,
cols: newest.end.column,
},
Mode::VisualLine => RecordedSelection::VisualLine {
rows: newest.end.row - newest.start.row,
},
Mode::VisualBlock => RecordedSelection::VisualBlock {
rows: newest.end.row.abs_diff(oldest.start.row),
cols: newest.end.column.abs_diff(oldest.start.column),
},
_ => RecordedSelection::None,
}
} else {
self.workspace_state.recorded_selection = RecordedSelection::None;
}
}
}
pub fn stop_recording(&mut self) {
if self.workspace_state.recording {
self.workspace_state.stop_recording_after_next_action = true;
}
}
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
self.start_recording(cx);
self.stop_recording();
}
fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
let state = self.state();
@@ -247,6 +353,12 @@ impl Vim {
}
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
if matches!(
operator,
Operator::Change | Operator::Delete | Operator::Replace
) {
self.start_recording(cx)
};
self.update_state(|state| state.operator_stack.push(operator));
self.sync_vim_settings(cx);
}
@@ -272,6 +384,12 @@ impl Vim {
}
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
if self.workspace_state.replaying {
if let Some(number) = self.workspace_state.recorded_count {
return Some(number);
}
}
if let Some(Operator::Number(number)) = self.active_operator() {
self.pop_operator(cx);
return Some(number);
@@ -295,14 +413,20 @@ impl Vim {
match Vim::read(cx).active_operator() {
Some(Operator::FindForward { before }) => {
let find = Motion::FindForward { before, text };
let find = Motion::FindForward {
before,
char: text.chars().next().unwrap(),
};
Vim::update(cx, |vim, _| {
vim.workspace_state.last_find = Some(find.clone())
});
motion::motion(find, cx)
}
Some(Operator::FindBackward { after }) => {
let find = Motion::FindBackward { after, text };
let find = Motion::FindBackward {
after,
char: text.chars().next().unwrap(),
};
Vim::update(cx, |vim, _| {
vim.workspace_state.last_find = Some(find.clone())
});

View File

@@ -277,6 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
vim.record_current_action(cx);
vim.update_active_editor(cx, |editor, cx| {
let mut original_columns: HashMap<_, _> = Default::default();
let line_mode = editor.selections.line_mode;
@@ -339,6 +340,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
vim.stop_recording();
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
let (display_map, selections) = editor.selections.all_adjusted_display(cx);

View File

@@ -16,3 +16,8 @@
{"Key":"shift-v"}
{"Key":"~"}
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
{"Put":{"state":"ˇaa\nbb\ncc"}}
{"Key":"ctrl-v"}
{"Key":"j"}
{"Key":"~"}
{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}

View File

@@ -0,0 +1,38 @@
{"Put":{"state":"ˇhello"}}
{"Key":"o"}
{"Key":"w"}
{"Key":"o"}
{"Key":"r"}
{"Key":"l"}
{"Key":"d"}
{"Key":"escape"}
{"Get":{"state":"hello\nworlˇd","mode":"Normal"}}
{"Key":"."}
{"Get":{"state":"hello\nworld\nworlˇd","mode":"Normal"}}
{"Key":"^"}
{"Key":"d"}
{"Key":"f"}
{"Key":"o"}
{"Key":"g"}
{"Key":"g"}
{"Key":"."}
{"Get":{"state":"ˇ\nworld\nrld","mode":"Normal"}}
{"Key":"j"}
{"Key":"y"}
{"Key":"y"}
{"Key":"p"}
{"Key":"shift-g"}
{"Key":"y"}
{"Key":"y"}
{"Key":"."}
{"Get":{"state":"\nworld\nworld\nrld\nˇrld","mode":"Normal"}}
{"Put":{"state":"ˇthe quick brown fox"}}
{"Key":"2"}
{"Key":"~"}
{"Key":"."}
{"Put":{"state":"THE ˇquick brown fox"}}
{"Key":"3"}
{"Key":"."}
{"Put":{"state":"THE QUIˇck brown fox"}}
{"Key":"."}
{"Put":{"state":"THE QUICK ˇbrown fox"}}

View File

@@ -0,0 +1,13 @@
{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
{"Key":"shift-j"}
{"Get":{"state":"oneˇ two\nthree\nfour\nfive\nsix\n","mode":"Normal"}}
{"Key":"3"}
{"Key":"shift-j"}
{"Get":{"state":"one two threeˇ four\nfive\nsix\n","mode":"Normal"}}
{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}}
{"Key":"j"}
{"Key":"v"}
{"Key":"3"}
{"Key":"j"}
{"Key":"shift-j"}
{"Get":{"state":"one\ntwo three fourˇ five\nsix\n","mode":"Normal"}}

View File

@@ -0,0 +1,51 @@
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"i"}
{"Key":"w"}
{"Key":"s"}
{"Key":"o"}
{"Key":"escape"}
{"Get":{"state":"ˇo quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"j"}
{"Key":"w"}
{"Key":"."}
{"Get":{"state":"o quick brown\nfox ˇops over\nthe lazy dog","mode":"Normal"}}
{"Key":"f"}
{"Key":"r"}
{"Key":"."}
{"Get":{"state":"o quick brown\nfox ops oveˇothe lazy dog","mode":"Normal"}}
{"Put":{"state":"the ˇquick brown\nfox jumps over\nfox jumps over\nfox jumps over\nthe lazy dog"}}
{"Key":"v"}
{"Key":"j"}
{"Key":"x"}
{"Get":{"state":"the ˇumps over\nfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"."}
{"Get":{"state":"the ˇumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"w"}
{"Key":"."}
{"Get":{"state":"the umps ˇumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"j"}
{"Key":"."}
{"Get":{"state":"the umps umps over\nthe ˇog","mode":"Normal"}}
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"ctrl-v"}
{"Key":"j"}
{"Key":"j"}
{"Key":"shift-i"}
{"Key":"o"}
{"Key":"escape"}
{"Get":{"state":"ˇothe quick brown\nofox jumps over\nothe lazy dog","mode":"Normal"}}
{"Key":"j"}
{"Key":"4"}
{"Key":"l"}
{"Key":"."}
{"Get":{"state":"othe quick brown\nofoxˇo jumps over\notheo lazy dog","mode":"Normal"}}
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
{"Key":"shift-v"}
{"Key":"shift-r"}
{"Key":"o"}
{"Key":"escape"}
{"Get":{"state":"ˇo\nfox jumps over\nthe lazy dog","mode":"Normal"}}
{"Key":"j"}
{"Key":"."}
{"Get":{"state":"o\nˇo\nthe lazy dog","mode":"Normal"}}

View File

@@ -53,3 +53,9 @@
{"Key":"i"}
{"Key":"w"}
{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}}
{"Key":"j"}
{"Key":"shift-f"}
{"Key":"e"}
{"Key":"f"}
{"Key":"r"}
{"Get":{"state":"fourteen \nfourteen chaˇr\n","mode":"Normal"}}

View File

@@ -171,6 +171,7 @@ pub trait Item: View {
None
}
}
fn as_searchable(&self, _: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
None
}
@@ -473,8 +474,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
for item_event in T::to_item_events(event).into_iter() {
match item_event {
ItemEvent::CloseItem => {
pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx))
.detach_and_log_err(cx);
pane.update(cx, |pane, cx| {
pane.close_item_by_id(
item.id(),
crate::SaveBehavior::PromptOnWrite,
cx,
)
})
.detach_and_log_err(cx);
return;
}

View File

@@ -43,6 +43,19 @@ use std::{
};
use theme::{Theme, ThemeSettings};
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub enum SaveBehavior {
/// ask before overwriting conflicting files (used by default with %s)
PromptOnConflict,
/// ask before writing any file that wouldn't be auto-saved (used by default with %w)
PromptOnWrite,
/// never prompt, write on conflict (used with vim's :w!)
SilentlyOverwrite,
/// skip all save-related behaviour (used with vim's :cq)
DontSave,
}
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivateItem(pub usize);
@@ -64,13 +77,17 @@ pub struct CloseItemsToTheRightById {
pub pane: WeakViewHandle<Pane>,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
pub struct CloseActiveItem {
pub save_behavior: Option<SaveBehavior>,
}
actions!(
pane,
[
ActivatePrevItem,
ActivateNextItem,
ActivateLastItem,
CloseActiveItem,
CloseInactiveItems,
CloseCleanItems,
CloseItemsToTheLeft,
@@ -86,7 +103,7 @@ actions!(
]
);
impl_actions!(pane, [ActivateItem]);
impl_actions!(pane, [ActivateItem, CloseActiveItem]);
const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
@@ -696,22 +713,29 @@ impl Pane {
pub fn close_active_item(
&mut self,
_: &CloseActiveItem,
action: &CloseActiveItem,
cx: &mut ViewContext<Self>,
) -> Option<Task<Result<()>>> {
if self.items.is_empty() {
return None;
}
let active_item_id = self.items[self.active_item_index].id();
Some(self.close_item_by_id(active_item_id, cx))
Some(self.close_item_by_id(
active_item_id,
action.save_behavior.unwrap_or(SaveBehavior::PromptOnWrite),
cx,
))
}
pub fn close_item_by_id(
&mut self,
item_id_to_close: usize,
save_behavior: SaveBehavior,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.close_items(cx, move |view_id| view_id == item_id_to_close)
self.close_items(cx, save_behavior, move |view_id| {
view_id == item_id_to_close
})
}
pub fn close_inactive_items(
@@ -724,7 +748,11 @@ impl Pane {
}
let active_item_id = self.items[self.active_item_index].id();
Some(self.close_items(cx, move |item_id| item_id != active_item_id))
Some(
self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_id != active_item_id
}),
)
}
pub fn close_clean_items(
@@ -737,7 +765,11 @@ impl Pane {
.filter(|item| !item.is_dirty(cx))
.map(|item| item.id())
.collect();
Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id)))
Some(
self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_ids.contains(&item_id)
}),
)
}
pub fn close_items_to_the_left(
@@ -762,7 +794,9 @@ impl Pane {
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
self.close_items(cx, move |item_id| item_ids.contains(&item_id))
self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_ids.contains(&item_id)
})
}
pub fn close_items_to_the_right(
@@ -788,7 +822,9 @@ impl Pane {
.take_while(|item| item.id() != item_id)
.map(|item| item.id())
.collect();
self.close_items(cx, move |item_id| item_ids.contains(&item_id))
self.close_items(cx, SaveBehavior::PromptOnWrite, move |item_id| {
item_ids.contains(&item_id)
})
}
pub fn close_all_items(
@@ -800,12 +836,13 @@ impl Pane {
return None;
}
Some(self.close_items(cx, move |_| true))
Some(self.close_items(cx, SaveBehavior::PromptOnWrite, |_| true))
}
pub fn close_items(
&mut self,
cx: &mut ViewContext<Pane>,
save_behavior: SaveBehavior,
should_close: impl 'static + Fn(usize) -> bool,
) -> Task<Result<()>> {
// Find the items to close.
@@ -858,8 +895,15 @@ impl Pane {
.any(|id| saved_project_items_ids.insert(*id));
if should_save
&& !Self::save_item(project.clone(), &pane, item_ix, &*item, true, &mut cx)
.await?
&& !Self::save_item(
project.clone(),
&pane,
item_ix,
&*item,
save_behavior,
&mut cx,
)
.await?
{
break;
}
@@ -954,13 +998,17 @@ impl Pane {
pane: &WeakViewHandle<Pane>,
item_ix: usize,
item: &dyn ItemHandle,
should_prompt_for_save: bool,
save_behavior: SaveBehavior,
cx: &mut AsyncAppContext,
) -> Result<bool> {
const CONFLICT_MESSAGE: &str =
"This file has changed on disk since you started editing it. Do you want to overwrite it?";
const DIRTY_MESSAGE: &str = "This file contains unsaved edits. Do you want to save it?";
if save_behavior == SaveBehavior::DontSave {
return Ok(true);
}
let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
(
item.has_conflict(cx),
@@ -971,18 +1019,22 @@ impl Pane {
});
if has_conflict && can_save {
let mut answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
&["Overwrite", "Discard", "Cancel"],
)
})?;
match answer.next().await {
Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
_ => return Ok(false),
if save_behavior == SaveBehavior::SilentlyOverwrite {
pane.update(cx, |_, cx| item.save(project, cx))?.await?;
} else {
let mut answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
&["Overwrite", "Discard", "Cancel"],
)
})?;
match answer.next().await {
Some(0) => pane.update(cx, |_, cx| item.save(project, cx))?.await?,
Some(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
_ => return Ok(false),
}
}
} else if is_dirty && (can_save || is_singleton) {
let will_autosave = cx.read(|cx| {
@@ -991,7 +1043,7 @@ impl Pane {
AutosaveSetting::OnFocusChange | AutosaveSetting::OnWindowChange
) && Self::can_autosave_item(&*item, cx)
});
let should_save = if should_prompt_for_save && !will_autosave {
let should_save = if save_behavior == SaveBehavior::PromptOnWrite && !will_autosave {
let mut answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
@@ -1113,7 +1165,12 @@ impl Pane {
AnchorCorner::TopLeft,
if is_active_item {
vec![
ContextMenuItem::action("Close Active Item", CloseActiveItem),
ContextMenuItem::action(
"Close Active Item",
CloseActiveItem {
save_behavior: None,
},
),
ContextMenuItem::action("Close Inactive Items", CloseInactiveItems),
ContextMenuItem::action("Close Clean Items", CloseCleanItems),
ContextMenuItem::action("Close Items To The Left", CloseItemsToTheLeft),
@@ -1128,8 +1185,12 @@ impl Pane {
move |cx| {
if let Some(pane) = pane.upgrade(cx) {
pane.update(cx, |pane, cx| {
pane.close_item_by_id(target_item_id, cx)
.detach_and_log_err(cx);
pane.close_item_by_id(
target_item_id,
SaveBehavior::PromptOnWrite,
cx,
)
.detach_and_log_err(cx);
})
}
}
@@ -1278,7 +1339,12 @@ impl Pane {
.on_click(MouseButton::Middle, {
let item_id = item.id();
move |_, pane, cx| {
pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
pane.close_item_by_id(
item_id,
SaveBehavior::PromptOnWrite,
cx,
)
.detach_and_log_err(cx);
}
})
.on_down(
@@ -1486,7 +1552,8 @@ impl Pane {
cx.window_context().defer(move |cx| {
if let Some(pane) = pane.upgrade(cx) {
pane.update(cx, |pane, cx| {
pane.close_item_by_id(item_id, cx).detach_and_log_err(cx);
pane.close_item_by_id(item_id, SaveBehavior::PromptOnWrite, cx)
.detach_and_log_err(cx);
});
}
});
@@ -2089,7 +2156,14 @@ mod tests {
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
pane.update(cx, |pane, cx| {
assert!(pane.close_active_item(&CloseActiveItem, cx).is_none())
assert!(pane
.close_active_item(
&CloseActiveItem {
save_behavior: None
},
cx
)
.is_none())
});
}
@@ -2339,31 +2413,59 @@ mod tests {
add_labeled_item(&pane, "1", false, cx);
assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
.unwrap()
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_active_item(
&CloseActiveItem {
save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "B*", "C", "D"], cx);
pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx));
assert_item_labels(&pane, ["A", "B", "C", "D*"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
.unwrap()
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_active_item(
&CloseActiveItem {
save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "B*", "C"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
.unwrap()
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_active_item(
&CloseActiveItem {
save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A", "C*"], cx);
pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx))
.unwrap()
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_active_item(
&CloseActiveItem {
save_behavior: None,
},
cx,
)
})
.unwrap()
.await
.unwrap();
assert_item_labels(&pane, ["A*"], cx);
}

View File

@@ -1308,13 +1308,15 @@ impl Workspace {
}
Ok(this
.update(&mut cx, |this, cx| this.save_all_internal(true, cx))?
.update(&mut cx, |this, cx| {
this.save_all_internal(SaveBehavior::PromptOnWrite, cx)
})?
.await?)
})
}
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
let save_all = self.save_all_internal(false, cx);
let save_all = self.save_all_internal(SaveBehavior::PromptOnConflict, cx);
Some(cx.foreground().spawn(async move {
save_all.await?;
Ok(())
@@ -1323,7 +1325,7 @@ impl Workspace {
fn save_all_internal(
&mut self,
should_prompt_to_save: bool,
save_behaviour: SaveBehavior,
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
if self.project.read(cx).is_read_only() {
@@ -1358,7 +1360,7 @@ impl Workspace {
&pane,
ix,
&*item,
should_prompt_to_save,
save_behaviour,
&mut cx,
)
.await?
@@ -4358,7 +4360,9 @@ mod tests {
let item1_id = item1.id();
let item3_id = item3.id();
let item4_id = item4.id();
pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id))
pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| {
[item1_id, item3_id, item4_id].contains(&id)
})
});
cx.foreground().run_until_parked();
@@ -4493,7 +4497,9 @@ mod tests {
// once for project entry 0, and once for project entry 2. After those two
// prompts, the task should complete.
let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true));
let close = left_pane.update(cx, |pane, cx| {
pane.close_items(cx, SaveBehavior::PromptOnWrite, move |_| true)
});
cx.foreground().run_until_parked();
left_pane.read_with(cx, |pane, cx| {
assert_eq!(
@@ -4609,9 +4615,11 @@ mod tests {
item.is_dirty = true;
});
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
})
.await
.unwrap();
assert!(!window.has_pending_prompt(cx));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
@@ -4630,8 +4638,9 @@ mod tests {
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
// Ensure autosave is prevented for deleted files also when closing the buffer.
let _close_items =
pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id));
let _close_items = pane.update(cx, |pane, cx| {
pane.close_items(cx, SaveBehavior::PromptOnWrite, move |id| id == item_id)
});
deterministic.run_until_parked();
assert!(window.has_pending_prompt(cx));
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));

View File

@@ -1,6 +1,5 @@
use anyhow::{anyhow, Result};
use anyhow::Result;
use async_trait::async_trait;
use futures::StreamExt;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
@@ -164,31 +163,16 @@ async fn get_cached_server_binary(
container_dir: PathBuf,
node: &dyn NodeRuntime,
) -> Option<LanguageServerBinary> {
(|| async move {
let mut last_version_dir = None;
let mut entries = fs::read_dir(&container_dir).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
if entry.file_type().await?.is_dir() {
last_version_dir = Some(entry.path());
}
}
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
let server_path = last_version_dir.join(SERVER_PATH);
if server_path.exists() {
Ok(LanguageServerBinary {
path: node.binary_path().await?,
arguments: server_binary_arguments(&server_path),
})
} else {
Err(anyhow!(
"missing executable in directory {:?}",
last_version_dir
))
}
})()
.await
.log_err()
let server_path = container_dir.join(SERVER_PATH);
if server_path.exists() {
Some(LanguageServerBinary {
path: node.binary_path().await.log_err()?,
arguments: server_binary_arguments(&server_path),
})
} else {
log::error!("missing executable in directory {:?}", server_path);
None
}
}
#[cfg(test)]

View File

@@ -262,6 +262,7 @@ impl LspAdapter for RustLspAdapter {
})
}
}
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
(|| async move {
let mut last = None;

View File

@@ -41,7 +41,12 @@ pub fn menus() -> Vec<Menu<'static>> {
MenuItem::action("Save", workspace::Save),
MenuItem::action("Save As…", workspace::SaveAs),
MenuItem::action("Save All", workspace::SaveAll),
MenuItem::action("Close Editor", workspace::CloseActiveItem),
MenuItem::action(
"Close Editor",
workspace::CloseActiveItem {
save_behavior: None,
},
),
MenuItem::action("Close Window", workspace::CloseWindow),
],
},

View File

@@ -733,7 +733,7 @@ mod tests {
use theme::{ThemeRegistry, ThemeSettings};
use workspace::{
item::{Item, ItemHandle},
open_new, open_paths, pane, NewFile, SplitDirection, WorkspaceHandle,
open_new, open_paths, pane, NewFile, SaveBehavior, SplitDirection, WorkspaceHandle,
};
#[gpui::test]
@@ -1495,7 +1495,12 @@ mod tests {
pane2_item.downcast::<Editor>().unwrap().downgrade()
});
cx.dispatch_action(window.into(), workspace::CloseActiveItem);
cx.dispatch_action(
window.into(),
workspace::CloseActiveItem {
save_behavior: None,
},
);
cx.foreground().run_until_parked();
workspace.read_with(cx, |workspace, _| {
@@ -1503,7 +1508,12 @@ mod tests {
assert_eq!(workspace.active_pane(), &pane_1);
});
cx.dispatch_action(window.into(), workspace::CloseActiveItem);
cx.dispatch_action(
window.into(),
workspace::CloseActiveItem {
save_behavior: None,
},
);
cx.foreground().run_until_parked();
window.simulate_prompt_answer(1, cx);
cx.foreground().run_until_parked();
@@ -1661,7 +1671,7 @@ mod tests {
pane.update(cx, |pane, cx| {
let editor3_id = editor3.id();
drop(editor3);
pane.close_item_by_id(editor3_id, cx)
pane.close_item_by_id(editor3_id, SaveBehavior::PromptOnWrite, cx)
})
.await
.unwrap();
@@ -1696,7 +1706,7 @@ mod tests {
pane.update(cx, |pane, cx| {
let editor2_id = editor2.id();
drop(editor2);
pane.close_item_by_id(editor2_id, cx)
pane.close_item_by_id(editor2_id, SaveBehavior::PromptOnWrite, cx)
})
.await
.unwrap();
@@ -1852,24 +1862,32 @@ mod tests {
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
// Close all the pane items in some arbitrary order.
pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_item_by_id(file1_item_id, SaveBehavior::PromptOnWrite, cx)
})
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_item_by_id(file4_item_id, SaveBehavior::PromptOnWrite, cx)
})
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_item_by_id(file2_item_id, SaveBehavior::PromptOnWrite, cx)
})
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx))
.await
.unwrap();
pane.update(cx, |pane, cx| {
pane.close_item_by_id(file3_item_id, SaveBehavior::PromptOnWrite, cx)
})
.await
.unwrap();
assert_eq!(active_path(&workspace, cx), None);
// Reopen all the closed items, ensuring they are reopened in the same order