vim: Add global marks (#25702)

AidanV and Conrad Irwin created

Closes https://github.com/zed-industries/zed/issues/13111

Release Notes:

- vim: Added global marks `'[A-Z]`
- vim: Added persistence for global (and local) marks. When re-opening
the same workspace your previous marks will be available.

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>

Change summary

Cargo.lock                          | 277 ++++++--------
crates/sqlez/src/bindable.rs        |   7 
crates/vim/Cargo.toml               |   3 
crates/vim/src/change_list.rs       |  15 
crates/vim/src/command.rs           |  10 
crates/vim/src/helix.rs             |   2 
crates/vim/src/insert.rs            |   2 
crates/vim/src/motion.rs            |   8 
crates/vim/src/normal.rs            |  14 
crates/vim/src/normal/change.rs     |   4 
crates/vim/src/normal/delete.rs     |   4 
crates/vim/src/normal/mark.rs       | 240 +++++++++++--
crates/vim/src/normal/paste.rs      |   2 
crates/vim/src/normal/substitute.rs |   2 
crates/vim/src/normal/yank.rs       |  27 +
crates/vim/src/state.rs             | 554 ++++++++++++++++++++++++++++++
crates/vim/src/vim.rs               |   6 
crates/vim/src/visual.rs            |  16 
18 files changed, 946 insertions(+), 247 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -257,9 +257,9 @@ checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4"
 
 [[package]]
 name = "anyhow"
-version = "1.0.97"
+version = "1.0.96"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
+checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
 
 [[package]]
 name = "approx"
@@ -284,7 +284,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -398,7 +398,7 @@ dependencies = [
  "ctor",
  "db",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "feature_flags",
  "fs",
  "futures 0.3.31",
@@ -578,7 +578,7 @@ dependencies = [
  "client",
  "collections",
  "context_server",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "fs",
  "futures 0.3.31",
  "gpui",
@@ -655,7 +655,7 @@ dependencies = [
  "collections",
  "context_server",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "feature_flags",
  "fs",
  "futures 0.3.31",
@@ -950,7 +950,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -1018,7 +1018,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -1075,13 +1075,13 @@ dependencies = [
 
 [[package]]
 name = "async-trait"
-version = "0.1.87"
+version = "0.1.86"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d556ec1359574147ec0c4fc5eb525f3f23263a592b1a9c07e0a75b427de55c97"
+checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -1285,25 +1285,27 @@ dependencies = [
 
 [[package]]
 name = "aws-lc-rs"
-version = "1.12.6"
+version = "1.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01"
+checksum = "4c2b7ddaa2c56a367ad27a094ad8ef4faacf8a617c2575acb2ba88949df999ca"
 dependencies = [
  "aws-lc-sys",
+ "paste",
  "zeroize",
 ]
 
 [[package]]
 name = "aws-lc-sys"
-version = "0.27.0"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6bbe221bbf523b625a4dd8585c7f38166e31167ec2ca98051dbcb4c3b6e825d2"
+checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491"
 dependencies = [
  "bindgen 0.69.5",
  "cc",
  "cmake",
  "dunce",
  "fs_extra",
+ "paste",
 ]
 
 [[package]]
@@ -1883,7 +1885,7 @@ dependencies = [
  "regex",
  "rustc-hash 1.1.0",
  "shlex",
- "syn 2.0.100",
+ "syn 2.0.90",
  "which 4.4.2",
 ]
 
@@ -1904,7 +1906,7 @@ dependencies = [
  "regex",
  "rustc-hash 1.1.0",
  "shlex",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -2015,7 +2017,7 @@ source = "git+https://github.com/kvark/blade?rev=b16f5c7bd873c7126f48c82c39e7ae6
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -2108,7 +2110,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -2142,7 +2144,7 @@ dependencies = [
  "anyhow",
  "clock",
  "ctor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures 0.3.31",
  "git2",
  "gpui",
@@ -2218,7 +2220,7 @@ checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -2480,7 +2482,7 @@ dependencies = [
  "quote",
  "serde",
  "serde_json",
- "syn 2.0.100",
+ "syn 2.0.90",
  "tempfile",
  "toml 0.8.20",
 ]
@@ -2498,7 +2500,7 @@ dependencies = [
  "quote",
  "serde",
  "serde_json",
- "syn 2.0.100",
+ "syn 2.0.90",
  "tempfile",
  "toml 0.8.20",
 ]
@@ -2652,9 +2654,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.32"
+version = "4.5.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
+checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -2662,9 +2664,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.32"
+version = "4.5.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
+checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
 dependencies = [
  "anstream",
  "anstyle",
@@ -2684,14 +2686,14 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.32"
+version = "4.5.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
+checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
 dependencies = [
  "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -2892,7 +2894,7 @@ dependencies = [
  "dashmap 6.1.0",
  "derive_more",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "envy",
  "extension",
  "file_finder",
@@ -3051,7 +3053,7 @@ dependencies = [
  "command_palette_hooks",
  "ctor",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "fuzzy",
  "go_to_line",
  "gpui",
@@ -3235,7 +3237,7 @@ dependencies = [
  "command_palette_hooks",
  "ctor",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "fs",
  "futures 0.3.31",
  "gpui",
@@ -3744,9 +3746,9 @@ dependencies = [
 
 [[package]]
 name = "ctor"
-version = "0.4.1"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07e9666f4a9a948d4f1dff0c08a4512b0f7c86414b23960104c243c10d79f4c3"
+checksum = "a7747ac3a66a06f4ee6718686c8ea976d2d05fb30ada93ebd76b3f9aef97257c"
 dependencies = [
  "ctor-proc-macro",
  "dtor",
@@ -3799,7 +3801,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "scratch",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -3812,7 +3814,7 @@ dependencies = [
  "codespan-reporting",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -3830,7 +3832,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -3965,7 +3967,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustc_version",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -3986,7 +3988,7 @@ dependencies = [
  "collections",
  "ctor",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "gpui",
  "language",
  "log",
@@ -4085,7 +4087,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -4134,9 +4136,9 @@ dependencies = [
 
 [[package]]
 name = "dtor"
-version = "0.0.5"
+version = "0.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "222ef136a1c687d4aa0395c175f2c4586e379924c352fd02f7870cf7de783c23"
+checksum = "8bf39a0bfd1f94d62ffdb2802a7e6244c0f34f6ebacf5d4c26547d08cd1d67a5"
 dependencies = [
  "dtor-proc-macro",
 ]
@@ -4205,7 +4207,7 @@ dependencies = [
  "ctor",
  "db",
  "emojis",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "file_icons",
  "fs",
  "futures 0.3.31",
@@ -4374,7 +4376,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -4408,14 +4410,14 @@ dependencies = [
 
 [[package]]
 name = "env_logger"
-version = "0.11.7"
+version = "0.11.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3716d7a920fb4fac5d84e9d4bce8ceb321e9414b4409da61b07b75c1e3d0697"
+checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
 dependencies = [
  "anstream",
  "anstyle",
  "env_filter",
- "jiff",
+ "humantime",
  "log",
 ]
 
@@ -4514,7 +4516,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "feature_flags",
  "fs",
  "gpui",
@@ -4619,7 +4621,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "clap",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "extension",
  "fs",
  "language",
@@ -4647,7 +4649,7 @@ dependencies = [
  "collections",
  "context_server_settings",
  "ctor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "extension",
  "fs",
  "futures 0.3.31",
@@ -4842,7 +4844,7 @@ dependencies = [
  "collections",
  "ctor",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "file_icons",
  "futures 0.3.31",
  "fuzzy",
@@ -5052,7 +5054,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -5313,7 +5315,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -5525,7 +5527,7 @@ dependencies = [
  "ctor",
  "db",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures 0.3.31",
  "fuzzy",
  "git",
@@ -5702,7 +5704,7 @@ dependencies = [
  "ctor",
  "derive_more",
  "embed-resource",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "etagere",
  "filedescriptor",
  "flume",
@@ -6093,7 +6095,7 @@ dependencies = [
  "markup5ever",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -6480,7 +6482,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -6657,7 +6659,7 @@ checksum = "0122b7114117e64a63ac49f752a5ca4624d534c7b1c7de796ac196381cd2d947"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -6767,7 +6769,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -6907,30 +6909,6 @@ version = "1.0.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
 
-[[package]]
-name = "jiff"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d699bc6dfc879fb1bf9bdff0d4c56f0884fc6f0d0eb0fba397a6d00cd9a6b85e"
-dependencies = [
- "jiff-static",
- "log",
- "portable-atomic",
- "portable-atomic-util",
- "serde",
-]
-
-[[package]]
-name = "jiff-static"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d16e75759ee0aa64c57a56acbf43916987b20c77373cb7e808979e02b93c9f9"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.100",
-]
-
 [[package]]
 name = "jni"
 version = "0.21.1"
@@ -7102,7 +7080,7 @@ dependencies = [
  "collections",
  "ctor",
  "ec4rs",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "fs",
  "futures 0.3.31",
  "fuzzy",
@@ -7282,7 +7260,7 @@ dependencies = [
  "collections",
  "copilot",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures 0.3.31",
  "gpui",
  "itertools 0.14.0",
@@ -7389,9 +7367,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
 
 [[package]]
 name = "libc"
-version = "0.2.171"
+version = "0.2.170"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
+checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
 
 [[package]]
 name = "libdbus-sys"
@@ -7542,7 +7520,7 @@ checksum = "71a98813fa0073a317ed6a8055dcd4722a49d9b862af828ee68449adb799b6be"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -7769,7 +7747,7 @@ dependencies = [
  "async-pipe",
  "collections",
  "ctor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures 0.3.31",
  "gpui",
  "log",
@@ -7914,7 +7892,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "assets",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "gpui",
  "language",
  "languages",
@@ -8018,9 +7996,9 @@ dependencies = [
 
 [[package]]
 name = "mdbook"
-version = "0.4.47"
+version = "0.4.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e1a8fe3a4a01f28dab245c474cb7b95ccb4d3d2f17a5419a3d949f474c45e84"
+checksum = "b07d36d96ffe1b5b16ddf2bc80b3b26bb7a498b2a6591061250bf0af8e8095ad"
 dependencies = [
  "ammonia",
  "anyhow",
@@ -8028,10 +8006,9 @@ dependencies = [
  "clap",
  "clap_complete",
  "elasticlunr-rs",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures-util",
  "handlebars 6.2.0",
- "hex",
  "ignore",
  "log",
  "memchr",
@@ -8044,7 +8021,6 @@ dependencies = [
  "regex",
  "serde",
  "serde_json",
- "sha2",
  "shlex",
  "tempfile",
  "tokio",
@@ -8276,7 +8252,7 @@ dependencies = [
  "clock",
  "collections",
  "ctor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures 0.3.31",
  "gpui",
  "indoc",
@@ -8641,7 +8617,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -8738,7 +8714,7 @@ dependencies = [
  "proc-macro-crate",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -9130,7 +9106,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -9217,7 +9193,7 @@ dependencies = [
  "proc-macro2",
  "proc-macro2-diagnostics",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -9332,7 +9308,7 @@ dependencies = [
  "by_address",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -9574,7 +9550,7 @@ dependencies = [
  "pest_meta",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -10021,7 +9997,7 @@ dependencies = [
  "phf_shared 0.11.2",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -10049,7 +10025,7 @@ dependencies = [
  "anyhow",
  "ctor",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "gpui",
  "menu",
  "schemars",
@@ -10083,7 +10059,7 @@ checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -10221,21 +10197,6 @@ version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
 
-[[package]]
-name = "portable-atomic"
-version = "1.11.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
-
-[[package]]
-name = "portable-atomic-util"
-version = "0.2.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
-dependencies = [
- "portable-atomic",
-]
-
 [[package]]
 name = "postage"
 version = "0.5.0"
@@ -10322,7 +10283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033"
 dependencies = [
  "proc-macro2",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -10353,14 +10314,14 @@ dependencies = [
  "proc-macro-error-attr2",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.94"
+version = "1.0.93"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
+checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
 dependencies = [
  "unicode-ident",
 ]
@@ -10373,7 +10334,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
  "version_check",
  "yansi",
 ]
@@ -10394,7 +10355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30"
 dependencies = [
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -10409,7 +10370,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "extension",
  "fancy-regex 0.14.0",
  "fs",
@@ -10636,7 +10597,7 @@ dependencies = [
  "prost 0.12.6",
  "prost-types 0.12.6",
  "regex",
- "syn 2.0.100",
+ "syn 2.0.90",
  "tempfile",
 ]
 
@@ -10663,7 +10624,7 @@ dependencies = [
  "itertools 0.12.1",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -10859,9 +10820,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.40"
+version = "1.0.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
 dependencies = [
  "proc-macro2",
 ]
@@ -11261,7 +11222,7 @@ dependencies = [
  "clap",
  "client",
  "clock",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "extension",
  "extension_host",
  "fork",
@@ -11322,7 +11283,7 @@ dependencies = [
  "collections",
  "command_palette_hooks",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "feature_flags",
  "file_icons",
  "futures 0.3.31",
@@ -11598,7 +11559,7 @@ dependencies = [
  "arrayvec",
  "criterion",
  "ctor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "gpui",
  "log",
  "rand 0.8.5",
@@ -11624,7 +11585,7 @@ dependencies = [
  "base64 0.22.1",
  "chrono",
  "collections",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures 0.3.31",
  "gpui",
  "parking_lot",
@@ -11642,9 +11603,9 @@ dependencies = [
 
 [[package]]
 name = "rsa"
-version = "0.9.8"
+version = "0.9.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
+checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
 dependencies = [
  "const-oid",
  "digest",
@@ -11706,7 +11667,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rust-embed-utils",
- "syn 2.0.100",
+ "syn 2.0.90",
  "walkdir",
 ]
 
@@ -11998,7 +11959,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "clap",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "schemars",
  "serde",
  "serde_json",
@@ -12007,9 +11968,9 @@ dependencies = [
 
 [[package]]
 name = "schemars"
-version = "0.8.22"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
 dependencies = [
  "dyn-clone",
  "indexmap",
@@ -12020,14 +11981,14 @@ dependencies = [
 
 [[package]]
 name = "schemars_derive"
-version = "0.8.22"
+version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
 dependencies = [
  "proc-macro2",
  "quote",
  "serde_derive_internals",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -12104,7 +12065,7 @@ dependencies = [
  "proc-macro-error2",
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -12145,7 +12106,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "sea-bae",
- "syn 2.0.100",
+ "syn 2.0.90",
  "unicode-ident",
 ]
 
@@ -12281,7 +12242,7 @@ dependencies = [
  "client",
  "clock",
  "collections",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "feature_flags",
  "fs",
  "futures 0.3.31",
@@ -12347,7 +12308,7 @@ checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -12358,7 +12319,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -12436,7 +12397,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -12971,7 +12932,7 @@ dependencies = [
  "quote",
  "sqlx-core",
  "sqlx-macros-core",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -12994,7 +12955,7 @@ dependencies = [
  "sqlx-mysql",
  "sqlx-postgres",
  "sqlx-sqlite",
- "syn 2.0.100",
+ "syn 2.0.90",
  "tempfile",
  "tokio",
  "url",
@@ -13253,7 +13214,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.100",
+ "syn 2.0.90",
 ]
 
 [[package]]
@@ -13268,7 +13229,7 @@ version = "0.1.0"
 dependencies = [
  "arrayvec",
  "ctor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "log",
  "rand 0.8.5",
  "rayon",
@@ -13282,7 +13243,7 @@ dependencies = [
  "client",
  "collections",
  "editor",
- "env_logger 0.11.7",
+ "env_logger 0.11.6",
  "futures 0.3.31",
  "gpui",
  "http_client",
@@ -13434,9 +13395,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.100"
+version = "2.0.90"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
+checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
 dependencies = [
  "proc-macro2",
  "quote",

crates/sqlez/src/bindable.rs 🔗

@@ -326,6 +326,13 @@ impl Bind for Arc<Path> {
         self.as_ref().bind(statement, start_index)
     }
 }
+impl Column for Arc<Path> {
+    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+        let blob = statement.column_blob(start_index)?;
+
+        PathBuf::try_from_bytes(blob).map(|path| (Arc::from(path.as_path()), start_index + 1))
+    }
+}
 
 impl StaticColumnCount for PathBuf {}
 impl Bind for PathBuf {

crates/vim/Cargo.toml 🔗

@@ -22,6 +22,7 @@ async-trait = { workspace = true, "optional" = true }
 collections.workspace = true
 command_palette.workspace = true
 command_palette_hooks.workspace = true
+db.workspace = true
 editor.workspace = true
 futures.workspace = true
 gpui.workspace = true
@@ -32,6 +33,7 @@ log.workspace = true
 multi_buffer.workspace = true
 nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", branch = "master", features = ["use_tokio"], optional = true }
 picker.workspace = true
+project.workspace = true
 regex.workspace = true
 schemars.workspace = true
 search.workspace = true
@@ -40,6 +42,7 @@ serde_derive.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 task.workspace = true
+text.workspace = true
 theme.workspace = true
 tokio = { version = "1.15", features = ["full"], optional = true }
 ui.workspace = true

crates/vim/src/change_list.rs 🔗

@@ -1,4 +1,6 @@
-use editor::{display_map::ToDisplayPoint, movement, scroll::Autoscroll, Bias, Direction, Editor};
+use editor::{
+    display_map::ToDisplayPoint, movement, scroll::Autoscroll, Anchor, Bias, Direction, Editor,
+};
 use gpui::{actions, Context, Window};
 
 use crate::{state::Mode, Vim};
@@ -48,8 +50,10 @@ impl Vim {
     }
 
     pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let Some((map, selections)) = self.update_editor(window, cx, |_, editor, _, cx| {
-            editor.selections.all_adjusted_display(cx)
+        let Some((map, selections, buffer)) = self.update_editor(window, cx, |_, editor, _, cx| {
+            let (map, selections) = editor.selections.all_adjusted_display(cx);
+            let buffer = editor.buffer().clone();
+            (map, selections, buffer)
         }) else {
             return;
         };
@@ -65,7 +69,7 @@ impl Vim {
             })
             .unwrap_or(false);
 
-        let new_positions = selections
+        let new_positions: Vec<Anchor> = selections
             .into_iter()
             .map(|s| {
                 let point = if self.mode == Mode::Insert {
@@ -81,7 +85,8 @@ impl Vim {
         if pop_state {
             self.change_list.pop();
         }
-        self.change_list.push(new_positions);
+        self.change_list.push(new_positions.clone());
+        self.set_mark(".".to_string(), new_positions, &buffer, window, cx)
     }
 }
 

crates/vim/src/command.rs 🔗

@@ -37,7 +37,7 @@ use crate::{
         JoinLines,
     },
     object::Object,
-    state::Mode,
+    state::{Mark, Mode},
     visual::VisualDeleteLine,
     ToggleRegistersView, Vim,
 };
@@ -284,6 +284,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
                     true,
                     true,
                     vec![Point::new(range.start.0, 0)..end],
+                    window,
                     cx,
                 )
             }
@@ -594,9 +595,14 @@ impl Position {
                 }
             }
             Position::Mark { name, offset } => {
-                let Some(mark) = vim.marks.get(&name.to_string()).and_then(|vec| vec.last()) else {
+                let Some(Mark::Local(anchors)) =
+                    vim.get_mark(&name.to_string(), editor, window, cx)
+                else {
                     return Err(anyhow!("mark {} not set", name));
                 };
+                let Some(mark) = anchors.last() else {
+                    return Err(anyhow!("mark {} contains empty anchors", name));
+                };
                 mark.to_point(&snapshot.buffer_snapshot)
                     .row
                     .saturating_add_signed(*offset)

crates/vim/src/helix.rs 🔗

@@ -254,7 +254,7 @@ impl Vim {
                 });
             });
 
-            vim.copy_selections_content(editor, false, cx);
+            vim.copy_selections_content(editor, false, window, cx);
             editor.insert("", window, cx);
         });
     }

crates/vim/src/insert.rs 🔗

@@ -25,7 +25,7 @@ impl Vim {
         let count = Vim::take_count(cx).unwrap_or(1);
         self.stop_recording_immediately(action.boxed_clone(), cx);
         if count <= 1 || Vim::globals(cx).dot_replaying {
-            self.create_mark("^".into(), false, window, cx);
+            self.create_mark("^".into(), window, cx);
             self.update_editor(window, cx, |_, editor, window, cx| {
                 editor.dismiss_menus_and_popups(false, window, cx);
                 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {

crates/vim/src/motion.rs 🔗

@@ -1889,7 +1889,7 @@ pub(crate) fn end_of_line(
     }
 }
 
-fn sentence_backwards(
+pub(crate) fn sentence_backwards(
     map: &DisplaySnapshot,
     point: DisplayPoint,
     mut times: usize,
@@ -1935,7 +1935,11 @@ fn sentence_backwards(
     DisplayPoint::zero()
 }
 
-fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
+pub(crate) fn sentence_forwards(
+    map: &DisplaySnapshot,
+    point: DisplayPoint,
+    mut times: usize,
+) -> DisplayPoint {
     let start = point.to_point(map).to_offset(&map.buffer_snapshot);
     let mut chars = map.buffer_chars_at(start).peekable();
 

crates/vim/src/normal.rs 🔗

@@ -18,7 +18,7 @@ use crate::{
     indent::IndentDirection,
     motion::{self, first_non_whitespace, next_line_end, right, Motion},
     object::Object,
-    state::{Mode, Operator},
+    state::{Mark, Mode, Operator},
     surrounds::SurroundsType,
     Vim,
 };
@@ -355,11 +355,13 @@ impl Vim {
         self.start_recording(cx);
         self.switch_mode(Mode::Insert, false, window, cx);
         self.update_editor(window, cx, |vim, editor, window, cx| {
-            if let Some(marks) = vim.marks.get("^") {
-                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
-                    s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
-                });
-            }
+            let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) else {
+                return;
+            };
+
+            editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+                s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
+            });
         });
     }
 

crates/vim/src/normal/change.rs 🔗

@@ -76,7 +76,7 @@ impl Vim {
                         }
                     });
                 });
-                vim.copy_selections_content(editor, motion.linewise(), cx);
+                vim.copy_selections_content(editor, motion.linewise(), window, cx);
                 editor.insert("", window, cx);
                 editor.refresh_inline_completion(true, false, window, cx);
             });
@@ -107,7 +107,7 @@ impl Vim {
                     });
                 });
                 if objects_found {
-                    vim.copy_selections_content(editor, false, cx);
+                    vim.copy_selections_content(editor, false, window, cx);
                     editor.insert("", window, cx);
                     editor.refresh_inline_completion(true, false, window, cx);
                 }

crates/vim/src/normal/delete.rs 🔗

@@ -60,7 +60,7 @@ impl Vim {
                         }
                     });
                 });
-                vim.copy_selections_content(editor, motion.linewise(), cx);
+                vim.copy_selections_content(editor, motion.linewise(), window, cx);
                 editor.insert("", window, cx);
 
                 // Fixup cursor position after the deletion
@@ -148,7 +148,7 @@ impl Vim {
                         }
                     });
                 });
-                vim.copy_selections_content(editor, false, cx);
+                vim.copy_selections_content(editor, false, window, cx);
                 editor.insert("", window, cx);
 
                 // Fixup cursor position after the deletion

crates/vim/src/normal/mark.rs 🔗

@@ -1,39 +1,34 @@
-use std::{ops::Range, sync::Arc};
+use std::{ops::Range, path::Path, sync::Arc};
 
 use editor::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     movement,
     scroll::Autoscroll,
-    Anchor, Bias, DisplayPoint,
+    Anchor, Bias, DisplayPoint, Editor, MultiBuffer,
 };
-use gpui::{Context, Window};
+use gpui::{Context, Entity, EntityId, UpdateGlobal, Window};
 use language::SelectionGoal;
+use text::Point;
+use ui::App;
+use workspace::OpenOptions;
 
 use crate::{
     motion::{self, Motion},
-    state::Mode,
+    state::{Mark, Mode, VimGlobals},
     Vim,
 };
 
 impl Vim {
-    pub fn create_mark(
-        &mut self,
-        text: Arc<str>,
-        tail: bool,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(anchors) = self.update_editor(window, cx, |_, editor, _, _| {
-            editor
+    pub fn create_mark(&mut self, text: Arc<str>, window: &mut Window, cx: &mut Context<Self>) {
+        self.update_editor(window, cx, |vim, editor, window, cx| {
+            let anchors = editor
                 .selections
                 .disjoint_anchors()
                 .iter()
-                .map(|s| if tail { s.tail() } else { s.head() })
-                .collect::<Vec<_>>()
-        }) else {
-            return;
-        };
-        self.marks.insert(text.to_string(), anchors);
+                .map(|s| s.head())
+                .collect::<Vec<_>>();
+            vim.set_mark(text.to_string(), anchors, editor.buffer(), window, cx);
+        });
         self.clear_operator(window, cx);
     }
 
@@ -55,7 +50,7 @@ impl Vim {
         let mut ends = vec![];
         let mut reversed = vec![];
 
-        self.update_editor(window, cx, |_, editor, _, cx| {
+        self.update_editor(window, cx, |vim, editor, window, cx| {
             let (map, selections) = editor.selections.all_display(cx);
             for selection in selections {
                 let end = movement::saturating_left(&map, selection.end);
@@ -69,13 +64,121 @@ impl Vim {
                 );
                 reversed.push(selection.reversed)
             }
+            vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx);
+            vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx);
         });
 
-        self.marks.insert("<".to_string(), starts);
-        self.marks.insert(">".to_string(), ends);
         self.stored_visual_mode.replace((mode, reversed));
     }
 
+    fn open_buffer_mark(
+        &mut self,
+        line: bool,
+        entity_id: EntityId,
+        anchors: Vec<Anchor>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(workspace) = self.workspace(window) else {
+            return;
+        };
+        workspace.update(cx, |workspace, cx| {
+            let item = workspace.items(cx).find(|item| {
+                item.act_as::<Editor>(cx)
+                    .is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id)
+            });
+            let Some(item) = item.cloned() else {
+                return;
+            };
+            if let Some(pane) = workspace.pane_for(item.as_ref()) {
+                pane.update(cx, |pane, cx| {
+                    if let Some(index) = pane.index_for_item(item.as_ref()) {
+                        pane.activate_item(index, true, true, window, cx);
+                    }
+                });
+            };
+
+            item.act_as::<Editor>(cx).unwrap().update(cx, |editor, cx| {
+                let map = editor.snapshot(window, cx);
+                let mut ranges: Vec<Range<Anchor>> = Vec::new();
+                for mut anchor in anchors {
+                    if line {
+                        let mut point = anchor.to_display_point(&map.display_snapshot);
+                        point = motion::first_non_whitespace(&map.display_snapshot, false, point);
+                        anchor = map
+                            .display_snapshot
+                            .buffer_snapshot
+                            .anchor_before(point.to_point(&map.display_snapshot));
+                    }
+
+                    if ranges.last() != Some(&(anchor..anchor)) {
+                        ranges.push(anchor..anchor);
+                    }
+                }
+
+                editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+                    s.select_anchor_ranges(ranges)
+                });
+            })
+        });
+        return;
+    }
+
+    fn open_path_mark(
+        &mut self,
+        line: bool,
+        path: Arc<Path>,
+        points: Vec<Point>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(workspace) = self.workspace(window) else {
+            return;
+        };
+        let task = workspace.update(cx, |workspace, cx| {
+            workspace.open_abs_path(
+                path.to_path_buf(),
+                OpenOptions {
+                    visible: Some(workspace::OpenVisible::All),
+                    focus: Some(true),
+                    ..Default::default()
+                },
+                window,
+                cx,
+            )
+        });
+        cx.spawn_in(window, |this, mut cx| async move {
+            let editor = task.await?;
+            this.update_in(&mut cx, |_, window, cx| {
+                if let Some(editor) = editor.act_as::<Editor>(cx) {
+                    editor.update(cx, |editor, cx| {
+                        let map = editor.snapshot(window, cx);
+                        let points: Vec<_> = points
+                            .into_iter()
+                            .map(|p| {
+                                if line {
+                                    let point = p.to_display_point(&map.display_snapshot);
+                                    motion::first_non_whitespace(
+                                        &map.display_snapshot,
+                                        false,
+                                        point,
+                                    )
+                                    .to_point(&map.display_snapshot)
+                                } else {
+                                    p
+                                }
+                            })
+                            .collect();
+                        editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+                            s.select_ranges(points.into_iter().map(|p| p..p))
+                        })
+                    })
+                }
+            })
+        })
+        .detach_and_log_err(cx);
+    }
+
     pub fn jump(
         &mut self,
         text: Arc<str>,
@@ -84,25 +187,22 @@ impl Vim {
         cx: &mut Context<Self>,
     ) {
         self.pop_operator(window, cx);
-
-        let anchors = match &*text {
-            "{" | "}" => self.update_editor(window, cx, |_, editor, _, cx| {
-                let (map, selections) = editor.selections.all_display(cx);
-                selections
-                    .into_iter()
-                    .map(|selection| {
-                        let point = if &*text == "{" {
-                            movement::start_of_paragraph(&map, selection.head(), 1)
-                        } else {
-                            movement::end_of_paragraph(&map, selection.head(), 1)
-                        };
-                        map.buffer_snapshot
-                            .anchor_before(point.to_offset(&map, Bias::Left))
-                    })
-                    .collect::<Vec<Anchor>>()
-            }),
-            "." => self.change_list.last().cloned(),
-            _ => self.marks.get(&*text).cloned(),
+        let mark = self
+            .update_editor(window, cx, |vim, editor, window, cx| {
+                vim.get_mark(&text, editor, window, cx)
+            })
+            .flatten();
+        let anchors = match mark {
+            None => None,
+            Some(Mark::Local(anchors)) => Some(anchors),
+            Some(Mark::Buffer(entity_id, anchors)) => {
+                self.open_buffer_mark(line, entity_id, anchors, window, cx);
+                return;
+            }
+            Some(Mark::Path(path, points)) => {
+                self.open_path_mark(line, path, points, window, cx);
+                return;
+            }
         };
 
         let Some(mut anchors) = anchors else { return };
@@ -144,7 +244,7 @@ impl Vim {
                     }
                 }
 
-                if !should_jump {
+                if !should_jump && !ranges.is_empty() {
                     editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
                         s.select_anchor_ranges(ranges)
                     });
@@ -158,6 +258,62 @@ impl Vim {
             }
         }
     }
+
+    pub fn set_mark(
+        &mut self,
+        name: String,
+        anchors: Vec<Anchor>,
+        buffer_entity: &Entity<MultiBuffer>,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        let Some(workspace) = self.workspace(window) else {
+            return;
+        };
+        let entity_id = workspace.entity_id();
+        Vim::update_globals(cx, |vim_globals, cx| {
+            let Some(marks_state) = vim_globals.marks.get(&entity_id) else {
+                return;
+            };
+            marks_state.update(cx, |ms, cx| {
+                ms.set_mark(name.clone(), buffer_entity, anchors, cx);
+            });
+        });
+    }
+
+    pub fn get_mark(
+        &self,
+        name: &str,
+        editor: &mut Editor,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Option<Mark> {
+        if matches!(name, "{" | "}" | "(" | ")") {
+            let (map, selections) = editor.selections.all_display(cx);
+            let anchors = selections
+                .into_iter()
+                .map(|selection| {
+                    let point = match name {
+                        "{" => movement::start_of_paragraph(&map, selection.head(), 1),
+                        "}" => movement::end_of_paragraph(&map, selection.head(), 1),
+                        "(" => motion::sentence_backwards(&map, selection.head(), 1),
+                        ")" => motion::sentence_forwards(&map, selection.head(), 1),
+                        _ => unreachable!(),
+                    };
+                    map.buffer_snapshot
+                        .anchor_before(point.to_offset(&map, Bias::Left))
+                })
+                .collect::<Vec<Anchor>>();
+            return Some(Mark::Local(anchors));
+        }
+        VimGlobals::update_global(cx, |globals, cx| {
+            let workspace_id = self.workspace(window)?.entity_id();
+            globals
+                .marks
+                .get_mut(&workspace_id)?
+                .update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx))
+        })
+    }
 }
 
 pub fn jump_motion(

crates/vim/src/normal/paste.rs 🔗

@@ -50,7 +50,7 @@ impl Vim {
                     .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine);
 
                 if !action.preserve_clipboard && vim.mode.is_visual() {
-                    vim.copy_selections_content(editor, vim.mode == Mode::VisualLine, cx);
+                    vim.copy_selections_content(editor, vim.mode == Mode::VisualLine, window, cx);
                 }
 
                 let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);

crates/vim/src/normal/substitute.rs 🔗

@@ -75,7 +75,7 @@ impl Vim {
                         }
                     })
                 });
-                vim.copy_selections_content(editor, line_mode, cx);
+                vim.copy_selections_content(editor, line_mode, window, cx);
                 let selections = editor.selections.all::<Point>(cx).into_iter();
                 let edits = selections.map(|selection| (selection.start..selection.end, ""));
                 editor.edit(edits, cx);

crates/vim/src/normal/yank.rs 🔗

@@ -36,7 +36,7 @@ impl Vim {
                         motion.expand_selection(map, selection, times, true, &text_layout_details);
                     });
                 });
-                vim.yank_selections_content(editor, motion.linewise(), cx);
+                vim.yank_selections_content(editor, motion.linewise(), window, cx);
                 editor.change_selections(None, window, cx, |s| {
                     s.move_with(|_, selection| {
                         let (head, goal) = original_positions.remove(&selection.id).unwrap();
@@ -66,7 +66,7 @@ impl Vim {
                         start_positions.insert(selection.id, start_position);
                     });
                 });
-                vim.yank_selections_content(editor, false, cx);
+                vim.yank_selections_content(editor, false, window, cx);
                 editor.change_selections(None, window, cx, |s| {
                     s.move_with(|_, selection| {
                         let (head, goal) = start_positions.remove(&selection.id).unwrap();
@@ -82,6 +82,7 @@ impl Vim {
         &mut self,
         editor: &mut Editor,
         linewise: bool,
+        window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
         self.copy_ranges(
@@ -94,6 +95,7 @@ impl Vim {
                 .iter()
                 .map(|s| s.range())
                 .collect(),
+            window,
             cx,
         )
     }
@@ -102,6 +104,7 @@ impl Vim {
         &mut self,
         editor: &mut Editor,
         linewise: bool,
+        window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
         self.copy_ranges(
@@ -114,6 +117,7 @@ impl Vim {
                 .iter()
                 .map(|s| s.range())
                 .collect(),
+            window,
             cx,
         )
     }
@@ -124,28 +128,35 @@ impl Vim {
         linewise: bool,
         is_yank: bool,
         selections: Vec<Range<Point>>,
+        window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
         let buffer = editor.buffer().read(cx).snapshot(cx);
-        let mut text = String::new();
-        let mut clipboard_selections = Vec::with_capacity(selections.len());
-        let mut ranges_to_highlight = Vec::new();
-
-        self.marks.insert(
+        self.set_mark(
             "[".to_string(),
             selections
                 .iter()
                 .map(|s| buffer.anchor_before(s.start))
                 .collect(),
+            editor.buffer(),
+            window,
+            cx,
         );
-        self.marks.insert(
+        self.set_mark(
             "]".to_string(),
             selections
                 .iter()
                 .map(|s| buffer.anchor_after(s.end))
                 .collect(),
+            editor.buffer(),
+            window,
+            cx,
         );
 
+        let mut text = String::new();
+        let mut clipboard_selections = Vec::with_capacity(selections.len());
+        let mut ranges_to_highlight = Vec::new();
+
         {
             let mut is_first = true;
             for selection in selections.iter() {

crates/vim/src/state.rs 🔗

@@ -3,27 +3,34 @@ use crate::normal::repeat::Replayer;
 use crate::surrounds::SurroundsType;
 use crate::{motion::Motion, object::Object};
 use crate::{ToggleRegistersView, UseSystemClipboard, Vim, VimSettings};
+use anyhow::Result;
 use collections::HashMap;
 use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor};
+use db::define_connection;
+use db::sqlez_macros::sql;
 use editor::display_map::{is_invisible, replacement};
-use editor::{Anchor, ClipboardSelection, Editor};
+use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer};
 use gpui::{
-    Action, App, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, Global, HighlightStyle,
-    StyledText, Task, TextStyle, WeakEntity,
+    Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, Entity, EntityId,
+    Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity,
 };
-use language::Point;
+use language::{Buffer, BufferEvent, BufferId, Point};
 use picker::{Picker, PickerDelegate};
+use project::{Project, ProjectItem, ProjectPath};
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsStore};
 use std::borrow::BorrowMut;
+use std::path::Path;
 use std::{fmt::Display, ops::Range, sync::Arc};
+use text::Bias;
 use theme::ThemeSettings;
 use ui::{
     h_flex, rems, ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement,
     SharedString, Styled, StyledTypography, Window,
 };
+use util::ResultExt;
 use workspace::searchable::Direction;
-use workspace::Workspace;
+use workspace::{Workspace, WorkspaceDb, WorkspaceId};
 
 #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
 pub enum Mode {
@@ -179,7 +186,7 @@ impl From<String> for Register {
     }
 }
 
-#[derive(Default, Clone)]
+#[derive(Default)]
 pub struct VimGlobals {
     pub last_find: Option<Motion>,
 
@@ -208,7 +215,399 @@ pub struct VimGlobals {
     pub recordings: HashMap<char, Vec<ReplayableAction>>,
 
     pub focused_vim: Option<WeakEntity<Vim>>,
+
+    pub marks: HashMap<EntityId, Entity<MarksState>>,
+}
+
+pub struct MarksState {
+    workspace: WeakEntity<Workspace>,
+
+    multibuffer_marks: HashMap<EntityId, HashMap<String, Vec<Anchor>>>,
+    buffer_marks: HashMap<BufferId, HashMap<String, Vec<text::Anchor>>>,
+    watched_buffers: HashMap<BufferId, (MarkLocation, Subscription, Subscription)>,
+
+    serialized_marks: HashMap<Arc<Path>, HashMap<String, Vec<Point>>>,
+    global_marks: HashMap<String, MarkLocation>,
+
+    _subscription: Subscription,
 }
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum MarkLocation {
+    Buffer(EntityId),
+    Path(Arc<Path>),
+}
+
+pub enum Mark {
+    Local(Vec<Anchor>),
+    Buffer(EntityId, Vec<Anchor>),
+    Path(Arc<Path>, Vec<Point>),
+}
+
+impl MarksState {
+    pub fn new(workspace: &Workspace, cx: &mut App) -> Entity<MarksState> {
+        cx.new(|cx| {
+            let buffer_store = workspace.project().read(cx).buffer_store().clone();
+            let subscription =
+                cx.subscribe(
+                    &buffer_store,
+                    move |this: &mut Self, _, event, cx| match event {
+                        project::buffer_store::BufferStoreEvent::BufferAdded(buffer) => {
+                            this.on_buffer_loaded(buffer, cx);
+                        }
+                        _ => {}
+                    },
+                );
+
+            let mut this = Self {
+                workspace: workspace.weak_handle(),
+                multibuffer_marks: HashMap::default(),
+                buffer_marks: HashMap::default(),
+                watched_buffers: HashMap::default(),
+                serialized_marks: HashMap::default(),
+                global_marks: HashMap::default(),
+                _subscription: subscription,
+            };
+
+            this.load(cx);
+            this
+        })
+    }
+
+    fn workspace_id(&self, cx: &App) -> Option<WorkspaceId> {
+        self.workspace
+            .read_with(cx, |workspace, _| workspace.database_id())
+            .ok()
+            .flatten()
+    }
+
+    fn project(&self, cx: &App) -> Option<Entity<Project>> {
+        self.workspace
+            .read_with(cx, |workspace, _| workspace.project().clone())
+            .ok()
+    }
+
+    fn load(&mut self, cx: &mut Context<Self>) {
+        cx.spawn(|this, mut cx| async move {
+            let Some(workspace_id) = this.update(&mut cx, |this, cx| this.workspace_id(cx))? else {
+                return Ok(());
+            };
+            let (marks, paths) = cx
+                .background_spawn(async move {
+                    let marks = DB.get_marks(workspace_id)?;
+                    let paths = DB.get_global_marks_paths(workspace_id)?;
+                    anyhow::Ok((marks, paths))
+                })
+                .await?;
+            this.update(&mut cx, |this, cx| this.loaded(marks, paths, cx))
+        })
+        .detach_and_log_err(cx);
+    }
+
+    fn loaded(
+        &mut self,
+        marks: Vec<SerializedMark>,
+        global_mark_paths: Vec<(String, Arc<Path>)>,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(project) = self.project(cx) else {
+            return;
+        };
+
+        for mark in marks {
+            self.serialized_marks
+                .entry(mark.path)
+                .or_default()
+                .insert(mark.name, mark.points);
+        }
+
+        for (name, path) in global_mark_paths {
+            self.global_marks
+                .insert(name, MarkLocation::Path(path.clone()));
+
+            let project_path = project
+                .read(cx)
+                .worktrees(cx)
+                .filter_map(|worktree| {
+                    let relative = path.strip_prefix(worktree.read(cx).abs_path()).ok()?;
+                    Some(ProjectPath {
+                        worktree_id: worktree.read(cx).id(),
+                        path: relative.into(),
+                    })
+                })
+                .next();
+            if let Some(buffer) = project_path
+                .and_then(|project_path| project.read(cx).get_open_buffer(&project_path, cx))
+            {
+                self.on_buffer_loaded(&buffer, cx)
+            }
+        }
+    }
+
+    pub fn on_buffer_loaded(&mut self, buffer_handle: &Entity<Buffer>, cx: &mut Context<Self>) {
+        let Some(project) = self.project(cx) else {
+            return;
+        };
+        let Some(project_path) = buffer_handle.read(cx).project_path(cx) else {
+            return;
+        };
+        let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) else {
+            return;
+        };
+        let abs_path: Arc<Path> = abs_path.into();
+
+        let Some(serialized_marks) = self.serialized_marks.get(&abs_path) else {
+            return;
+        };
+
+        let mut loaded_marks = HashMap::default();
+        let buffer = buffer_handle.read(cx);
+        for (name, points) in serialized_marks.iter() {
+            loaded_marks.insert(
+                name.clone(),
+                points
+                    .iter()
+                    .map(|point| buffer.anchor_before(buffer.clip_point(*point, Bias::Left)))
+                    .collect(),
+            );
+        }
+        self.buffer_marks.insert(buffer.remote_id(), loaded_marks);
+        self.watch_buffer(MarkLocation::Path(abs_path), buffer_handle, cx)
+    }
+
+    fn serialize_buffer_marks(
+        &mut self,
+        path: Arc<Path>,
+        buffer: &Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) {
+        let new_points: HashMap<String, Vec<Point>> =
+            if let Some(anchors) = self.buffer_marks.get(&buffer.read(cx).remote_id()) {
+                anchors
+                    .iter()
+                    .map(|(name, anchors)| {
+                        (
+                            name.clone(),
+                            buffer
+                                .read(cx)
+                                .summaries_for_anchors::<Point, _>(anchors)
+                                .collect(),
+                        )
+                    })
+                    .collect()
+            } else {
+                HashMap::default()
+            };
+        let old_points = self.serialized_marks.get(&path.clone());
+        if old_points == Some(&new_points) {
+            return;
+        }
+        let mut to_write = HashMap::default();
+
+        for (key, value) in &new_points {
+            if self.is_global_mark(key) {
+                if self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) {
+                    if let Some(workspace_id) = self.workspace_id(cx) {
+                        let path = path.clone();
+                        let key = key.clone();
+                        cx.background_spawn(async move {
+                            DB.set_global_mark_path(workspace_id, key, path).await
+                        })
+                        .detach_and_log_err(cx);
+                    }
+
+                    self.global_marks
+                        .insert(key.clone(), MarkLocation::Path(path.clone()));
+                }
+            }
+            if old_points.and_then(|o| o.get(key)) != Some(value) {
+                to_write.insert(key.clone(), value.clone());
+            }
+        }
+
+        self.serialized_marks.insert(path.clone(), new_points);
+
+        if let Some(workspace_id) = self.workspace_id(cx) {
+            cx.background_spawn(async move {
+                DB.set_marks(workspace_id, path.clone(), to_write).await?;
+                anyhow::Ok(())
+            })
+            .detach_and_log_err(cx);
+        }
+    }
+
+    fn is_global_mark(&self, key: &str) -> bool {
+        key.chars()
+            .next()
+            .is_some_and(|c| c.is_uppercase() || c.is_digit(10))
+    }
+
+    fn rename_buffer(
+        &mut self,
+        old_path: MarkLocation,
+        new_path: Arc<Path>,
+        buffer: &Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) {
+        if let MarkLocation::Buffer(entity_id) = old_path {
+            if let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) {
+                let buffer_marks = old_marks
+                    .into_iter()
+                    .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect()))
+                    .collect();
+                self.buffer_marks
+                    .insert(buffer.read(cx).remote_id(), buffer_marks);
+            }
+        }
+        self.watch_buffer(MarkLocation::Path(new_path.clone()), buffer, cx);
+        self.serialize_buffer_marks(new_path, buffer, cx);
+    }
+
+    fn path_for_buffer(&self, buffer: &Entity<Buffer>, cx: &App) -> Option<Arc<Path>> {
+        let project_path = buffer.read(cx).project_path(cx)?;
+        let project = self.project(cx)?;
+        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
+        Some(abs_path.into())
+    }
+
+    fn points_at(
+        &self,
+        location: &MarkLocation,
+        multi_buffer: &Entity<MultiBuffer>,
+        cx: &App,
+    ) -> bool {
+        match location {
+            MarkLocation::Buffer(entity_id) => entity_id == &multi_buffer.entity_id(),
+            MarkLocation::Path(path) => {
+                let Some(singleton) = multi_buffer.read(cx).as_singleton() else {
+                    return false;
+                };
+                self.path_for_buffer(&singleton, cx).as_ref() == Some(path)
+            }
+        }
+    }
+
+    pub fn watch_buffer(
+        &mut self,
+        mark_location: MarkLocation,
+        buffer_handle: &Entity<Buffer>,
+        cx: &mut Context<Self>,
+    ) {
+        let on_change = cx.subscribe(buffer_handle, move |this, buffer, event, cx| match event {
+            BufferEvent::Edited => {
+                if let Some(path) = this.path_for_buffer(&buffer, cx) {
+                    this.serialize_buffer_marks(path, &buffer, cx);
+                }
+            }
+            BufferEvent::FileHandleChanged => {
+                let buffer_id = buffer.read(cx).remote_id();
+                if let Some(old_path) = this
+                    .watched_buffers
+                    .get(&buffer_id.clone())
+                    .map(|(path, _, _)| path.clone())
+                {
+                    if let Some(new_path) = this.path_for_buffer(&buffer, cx) {
+                        this.rename_buffer(old_path, new_path, &buffer, cx)
+                    }
+                }
+            }
+            _ => {}
+        });
+
+        let on_release = cx.observe_release(buffer_handle, |this, buffer, _| {
+            this.watched_buffers.remove(&buffer.remote_id());
+            this.buffer_marks.remove(&buffer.remote_id());
+        });
+
+        self.watched_buffers.insert(
+            buffer_handle.read(cx).remote_id(),
+            (mark_location, on_change, on_release),
+        );
+    }
+
+    pub fn set_mark(
+        &mut self,
+        name: String,
+        multibuffer: &Entity<MultiBuffer>,
+        anchors: Vec<Anchor>,
+        cx: &mut Context<Self>,
+    ) {
+        let buffer = multibuffer.read(cx).as_singleton();
+        let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(&b, cx));
+
+        let Some(abs_path) = abs_path else {
+            self.multibuffer_marks
+                .entry(multibuffer.entity_id())
+                .or_default()
+                .insert(name.clone(), anchors);
+            if self.is_global_mark(&name) {
+                self.global_marks
+                    .insert(name.clone(), MarkLocation::Buffer(multibuffer.entity_id()));
+            }
+            if let Some(buffer) = buffer {
+                let buffer_id = buffer.read(cx).remote_id();
+                if !self.watched_buffers.contains_key(&buffer_id) {
+                    self.watch_buffer(MarkLocation::Buffer(multibuffer.entity_id()), &buffer, cx)
+                }
+            }
+            return;
+        };
+        let buffer = buffer.unwrap();
+
+        let buffer_id = buffer.read(cx).remote_id();
+        self.buffer_marks.entry(buffer_id).or_default().insert(
+            name.clone(),
+            anchors
+                .into_iter()
+                .map(|anchor| anchor.text_anchor)
+                .collect(),
+        );
+        if !self.watched_buffers.contains_key(&buffer_id) {
+            self.watch_buffer(MarkLocation::Path(abs_path.clone()), &buffer, cx)
+        }
+        self.serialize_buffer_marks(abs_path, &buffer, cx)
+    }
+
+    pub fn get_mark(
+        &self,
+        name: &str,
+        multi_buffer: &Entity<MultiBuffer>,
+        cx: &App,
+    ) -> Option<Mark> {
+        let target = self.global_marks.get(name);
+
+        if !self.is_global_mark(name) || target.is_some_and(|t| self.points_at(t, multi_buffer, cx))
+        {
+            if let Some(anchors) = self.multibuffer_marks.get(&multi_buffer.entity_id()) {
+                return Some(Mark::Local(anchors.get(name)?.clone()));
+            }
+
+            let singleton = multi_buffer.read(cx).as_singleton()?;
+            let excerpt_id = *multi_buffer.read(cx).excerpt_ids().first().unwrap();
+            let buffer_id = singleton.read(cx).remote_id();
+            if let Some(anchors) = self.buffer_marks.get(&buffer_id) {
+                let text_anchors = anchors.get(name)?;
+                let anchors = text_anchors
+                    .into_iter()
+                    .map(|anchor| Anchor::in_buffer(excerpt_id, buffer_id, *anchor))
+                    .collect();
+                return Some(Mark::Local(anchors));
+            }
+        }
+
+        match target? {
+            MarkLocation::Buffer(entity_id) => {
+                let anchors = self.multibuffer_marks.get(&entity_id)?;
+                return Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone()));
+            }
+            MarkLocation::Path(path) => {
+                let points = self.serialized_marks.get(path)?;
+                return Some(Mark::Path(path.clone(), points.get(name)?.clone()));
+            }
+        }
+    }
+}
+
 impl Global for VimGlobals {}
 
 impl VimGlobals {
@@ -228,8 +627,15 @@ impl VimGlobals {
         })
         .detach();
 
+        let mut was_enabled = None;
+
         cx.observe_global::<SettingsStore>(move |cx| {
-            if Vim::enabled(cx) {
+            let is_enabled = Vim::enabled(cx);
+            if was_enabled == Some(is_enabled) {
+                return;
+            }
+            was_enabled = Some(is_enabled);
+            if is_enabled {
                 KeyBinding::set_vim_mode(cx, true);
                 CommandPaletteFilter::update_global(cx, |filter, _| {
                     filter.show_namespace(Vim::NAMESPACE);
@@ -237,6 +643,17 @@ impl VimGlobals {
                 CommandPaletteInterceptor::update_global(cx, |interceptor, _| {
                     interceptor.set(Box::new(command_interceptor));
                 });
+                for window in cx.windows() {
+                    if let Some(workspace) = window.downcast::<Workspace>() {
+                        workspace
+                            .update(cx, |workspace, _, cx| {
+                                Vim::update_globals(cx, |globals, cx| {
+                                    globals.register_workspace(workspace, cx)
+                                });
+                            })
+                            .ok();
+                    }
+                }
             } else {
                 KeyBinding::set_vim_mode(cx, false);
                 *Vim::globals(cx) = VimGlobals::default();
@@ -249,6 +666,21 @@ impl VimGlobals {
             }
         })
         .detach();
+        cx.observe_new(|workspace: &mut Workspace, _, cx| {
+            Vim::update_globals(cx, |globals, cx| globals.register_workspace(workspace, cx));
+        })
+        .detach()
+    }
+
+    fn register_workspace(&mut self, workspace: &Workspace, cx: &mut Context<Workspace>) {
+        let entity_id = cx.entity_id();
+        self.marks.insert(entity_id, MarksState::new(workspace, cx));
+        cx.observe_release(&cx.entity(), move |_, _, cx| {
+            Vim::update_globals(cx, |globals, _| {
+                globals.marks.remove(&entity_id);
+            })
+        })
+        .detach();
     }
 
     pub(crate) fn write_registers(
@@ -799,3 +1231,111 @@ impl RegistersView {
             .modal(true)
     }
 }
+
+define_connection! (
+    pub static ref DB: VimDb<WorkspaceDb> = &[
+        sql! (
+            CREATE TABLE vim_marks (
+              workspace_id INTEGER,
+              mark_name TEXT,
+              path BLOB,
+              value TEXT
+            );
+            CREATE UNIQUE INDEX idx_vim_marks ON vim_marks (workspace_id, mark_name, path);
+        ),
+        sql! (
+            CREATE TABLE vim_global_marks_paths(
+                workspace_id INTEGER,
+                mark_name TEXT,
+                path BLOB
+            );
+            CREATE UNIQUE INDEX idx_vim_global_marks_paths
+            ON vim_global_marks_paths(workspace_id, mark_name);
+        ),
+    ];
+);
+
+struct SerializedMark {
+    path: Arc<Path>,
+    name: String,
+    points: Vec<Point>,
+}
+
+impl VimDb {
+    pub(crate) async fn set_marks(
+        &self,
+        workspace_id: WorkspaceId,
+        path: Arc<Path>,
+        marks: HashMap<String, Vec<Point>>,
+    ) -> Result<()> {
+        let result = self
+            .write(move |conn| {
+                let mut query = conn.exec_bound(sql!(
+                    INSERT OR REPLACE INTO vim_marks
+                        (workspace_id, mark_name, path, value)
+                    VALUES
+                        (?, ?, ?, ?)
+                ))?;
+                for (mark_name, value) in marks {
+                    let pairs: Vec<(u32, u32)> = value
+                        .into_iter()
+                        .map(|point| (point.row, point.column))
+                        .collect();
+                    let serialized = serde_json::to_string(&pairs)?;
+                    query((workspace_id, mark_name, path.clone(), serialized))?;
+                }
+                Ok(())
+            })
+            .await;
+        result
+    }
+
+    fn get_marks(&self, workspace_id: WorkspaceId) -> Result<Vec<SerializedMark>> {
+        let result: Vec<(Arc<Path>, String, String)> = self.select_bound(sql!(
+            SELECT path, mark_name, value FROM vim_marks
+                WHERE workspace_id = ?
+        ))?(workspace_id)?;
+
+        Ok(result
+            .into_iter()
+            .filter_map(|(path, name, value)| {
+                let pairs: Vec<(u32, u32)> = serde_json::from_str(&value).log_err()?;
+                Some(SerializedMark {
+                    path,
+                    name,
+                    points: pairs
+                        .into_iter()
+                        .map(|(row, column)| Point { row, column })
+                        .collect(),
+                })
+            })
+            .collect())
+    }
+
+    pub(crate) async fn set_global_mark_path(
+        &self,
+        workspace_id: WorkspaceId,
+        mark_name: String,
+        path: Arc<Path>,
+    ) -> Result<()> {
+        self.write(move |conn| {
+            conn.exec_bound(sql!(
+                INSERT OR REPLACE INTO vim_global_marks_paths
+                    (workspace_id, mark_name, path)
+                VALUES
+                    (?, ?, ?)
+            ))?((workspace_id, mark_name, path))
+        })
+        .await
+    }
+
+    pub fn get_global_marks_paths(
+        &self,
+        workspace_id: WorkspaceId,
+    ) -> Result<Vec<(String, Arc<Path>)>> {
+        self.select_bound(sql!(
+        SELECT mark_name, path FROM vim_global_marks_paths
+            WHERE workspace_id = ?
+        ))?(workspace_id)
+    }
+}

crates/vim/src/vim.rs 🔗

@@ -26,7 +26,7 @@ use editor::{
     Anchor, Bias, Editor, EditorEvent, EditorMode, EditorSettings, ToPoint,
 };
 use gpui::{
-    actions, impl_actions, Action, App, AppContext as _, Axis, Context, Entity, EventEmitter,
+    actions, impl_actions, Action, App, AppContext, Axis, Context, Entity, EventEmitter,
     KeyContext, KeystrokeEvent, Render, Subscription, Task, WeakEntity, Window,
 };
 use insert::{NormalBefore, TemporaryNormal};
@@ -314,7 +314,6 @@ pub(crate) struct Vim {
     operator_stack: Vec<Operator>,
     pub(crate) replacements: Vec<(Range<editor::Anchor>, String)>,
 
-    pub(crate) marks: HashMap<String, Vec<Anchor>>,
     pub(crate) stored_visual_mode: Option<(Mode, Vec<bool>)>,
     pub(crate) change_list: Vec<Vec<Anchor>>,
     pub(crate) change_list_position: Option<usize>,
@@ -362,7 +361,6 @@ impl Vim {
             operator_stack: Vec::new(),
             replacements: Vec::new(),
 
-            marks: HashMap::default(),
             stored_visual_mode: None,
             change_list: Vec::new(),
             change_list_position: None,
@@ -1573,7 +1571,7 @@ impl Vim {
                 }
                 _ => self.clear_operator(window, cx),
             },
-            Some(Operator::Mark) => self.create_mark(text, false, window, cx),
+            Some(Operator::Mark) => self.create_mark(text, window, cx),
             Some(Operator::RecordRegister) => {
                 self.record_register(text.chars().next().unwrap(), window, cx)
             }

crates/vim/src/visual.rs 🔗

@@ -17,7 +17,7 @@ use workspace::searchable::Direction;
 use crate::{
     motion::{first_non_whitespace, next_line_end, start_of_line, Motion},
     object::Object,
-    state::{Mode, Operator},
+    state::{Mark, Mode, Operator},
     Vim,
 };
 
@@ -107,14 +107,20 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
         let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else {
             return;
         };
-        let Some((start, end)) = vim.marks.get("<").zip(vim.marks.get(">")) else {
+        let marks = vim
+            .update_editor(window, cx, |vim, editor, window, cx| {
+                vim.get_mark("<", editor, window, cx)
+                    .zip(vim.get_mark(">", editor, window, cx))
+            })
+            .flatten();
+        let Some((Mark::Local(start), Mark::Local(end))) = marks else {
             return;
         };
         let ranges = start
             .iter()
             .zip(end)
             .zip(reversed)
-            .map(|((start, end), reversed)| (*start, *end, reversed))
+            .map(|((start, end), reversed)| (*start, end, reversed))
             .collect::<Vec<_>>();
 
         if vim.mode.is_visual() {
@@ -499,7 +505,7 @@ impl Vim {
                         selection.goal = SelectionGoal::None;
                     });
                 });
-                vim.copy_selections_content(editor, line_mode, cx);
+                vim.copy_selections_content(editor, line_mode, window, cx);
                 editor.insert("", window, cx);
 
                 // Fixup cursor position after the deletion
@@ -528,7 +534,7 @@ impl Vim {
         self.update_editor(window, cx, |vim, editor, window, cx| {
             let line_mode = line_mode || editor.selections.line_mode;
             editor.selections.line_mode = line_mode;
-            vim.yank_selections_content(editor, line_mode, cx);
+            vim.yank_selections_content(editor, line_mode, window, cx);
             editor.change_selections(None, window, cx, |s| {
                 s.move_with(|map, selection| {
                     if line_mode {