catchup with main

This commit is contained in:
KCaverly 2023-07-26 09:50:38 -04:00
commit 0ac919f6e0
112 changed files with 4590 additions and 1029 deletions

208
Cargo.lock generated
View File

@ -362,7 +362,7 @@ dependencies = [
"async-lock",
"async-task",
"concurrent-queue",
"fastrand",
"fastrand 1.9.0",
"futures-lite",
"slab",
]
@ -481,7 +481,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -529,7 +529,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -566,13 +566,13 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.71"
version = "0.1.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -830,7 +830,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
"syn 2.0.26",
"syn 2.0.27",
"which",
]
@ -907,7 +907,7 @@ dependencies = [
"async-lock",
"async-task",
"atomic-waker",
"fastrand",
"fastrand 1.9.0",
"futures-lite",
"log",
]
@ -1070,6 +1070,10 @@ dependencies = [
"media",
"postage",
"project",
"schemars",
"serde",
"serde_derive",
"serde_json",
"settings",
"util",
]
@ -1243,9 +1247,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.3.15"
version = "4.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f644d0dac522c8b05ddc39aaaccc5b136d5dc4ff216610c5641e3be5becf56c"
checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d"
dependencies = [
"clap_builder",
"clap_derive 4.3.12",
@ -1254,9 +1258,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.3.15"
version = "4.3.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af410122b9778e024f9e0fb35682cc09cc3f85cad5e8d3ba8f47a9702df6e73d"
checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1"
dependencies = [
"anstream",
"anstyle",
@ -1286,7 +1290,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -1970,9 +1974,9 @@ dependencies = [
[[package]]
name = "curl-sys"
version = "0.4.63+curl-8.1.2"
version = "0.4.64+curl-8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc"
checksum = "f96069f0b1cb1241c838740659a771ef143363f52772a9ce1bd9c04c75eee0dc"
dependencies = [
"cc",
"libc",
@ -2262,9 +2266,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.8.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encoding_rs"
@ -2413,6 +2417,12 @@ dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
[[package]]
name = "feedback"
version = "0.1.0"
@ -2771,7 +2781,7 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
dependencies = [
"fastrand",
"fastrand 1.9.0",
"futures-core",
"futures-io",
"memchr",
@ -2788,7 +2798,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -3250,9 +3260,9 @@ dependencies = [
[[package]]
name = "http-range-header"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29"
checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f"
[[package]]
name = "httparse"
@ -3893,9 +3903,9 @@ dependencies = [
[[package]]
name = "libz-sys"
version = "1.1.9"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ee889ecc9568871456d42f603d6a0ce59ff328d291063a45cbdf0036baf6db"
checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b"
dependencies = [
"cc",
"libc",
@ -4561,9 +4571,9 @@ dependencies = [
[[package]]
name = "num-traits"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
dependencies = [
"autocfg",
"libm",
@ -4722,7 +4732,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -5010,7 +5020,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -5158,12 +5168,12 @@ dependencies = [
[[package]]
name = "prettyplease"
version = "0.2.10"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92139198957b410250d43fad93e630d956499a625c527eda65175c8680f83387"
checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62"
dependencies = [
"proc-macro2",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -5288,6 +5298,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"client",
"collections",
"context_menu",
"db",
"drag_and_drop",
@ -5491,9 +5502,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.31"
version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
dependencies = [
"proc-macro2",
]
@ -5895,9 +5906,9 @@ dependencies = [
[[package]]
name = "rmp"
version = "0.8.11"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f"
checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
dependencies = [
"byteorder",
"num-traits",
@ -5906,9 +5917,9 @@ dependencies = [
[[package]]
name = "rmpv"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de8813b3a2f95c5138fe5925bfb8784175d88d6bff059ba8ce090aa891319754"
checksum = "2e0e0214a4a2b444ecce41a4025792fc31f77c7bb89c46d253953ea8c65701ec"
dependencies = [
"num-traits",
"rmp",
@ -6034,7 +6045,7 @@ dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.26",
"syn 2.0.27",
"walkdir",
]
@ -6419,6 +6430,7 @@ name = "search"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags 1.3.2",
"client",
"collections",
"editor",
@ -6445,9 +6457,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.9.1"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
@ -6458,9 +6470,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.9.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys 0.8.3",
"libc",
@ -6538,22 +6550,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99"
[[package]]
name = "serde"
version = "1.0.171"
version = "1.0.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.171"
version = "1.0.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -6602,13 +6614,13 @@ dependencies = [
[[package]]
name = "serde_repr"
version = "0.1.14"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d89a8107374290037607734c0b73a85db7ed80cae314b3c5791f192a496e731"
checksum = "e168eaaf71e8f9bd6037feb05190485708e019f4fd87d161b3c0a0d37daf85e5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -6749,9 +6761,9 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "signal-hook"
version = "0.3.16"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b824b6e687aff278cdbf3b36f07aa52d4bd4099699324d5da86a2ebce3aa00b3"
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
dependencies = [
"libc",
"signal-hook-registry",
@ -7276,9 +7288,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.26"
version = "2.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
checksum = "b60f673f44a8255b9c8c657daf66a596d435f2da81a555b06dc644d080ba45e0"
dependencies = [
"proc-macro2",
"quote",
@ -7346,9 +7358,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "target-lexicon"
version = "0.12.9"
version = "0.12.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8e77cb757a61f51b947ec4a7e3646efd825b73561db1c232a8ccb639e611a0"
checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e"
[[package]]
name = "tempdir"
@ -7362,15 +7374,14 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.6.0"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"fastrand",
"fastrand 2.0.0",
"redox_syscall 0.3.5",
"rustix 0.37.23",
"rustix 0.38.4",
"windows-sys",
]
@ -7517,22 +7528,22 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42"
checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -7721,7 +7732,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -7926,7 +7937,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]
@ -8000,11 +8011,20 @@ dependencies = [
"regex",
]
[[package]]
name = "tree-sitter-bash"
version = "0.19.0"
source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=1b0321ee85701d5036c334a6f04761cdc672e64c#1b0321ee85701d5036c334a6f04761cdc672e64c"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-c"
version = "0.20.2"
version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cca211f4827d4b4dc79f388bf67b6fa3bc8a8cfa642161ef24f99f371ba34c7b"
checksum = "fa1bb73a4101c88775e4fefcd0543ee25e192034484a5bd45cb99eefb997dca9"
dependencies = [
"cc",
"tree-sitter",
@ -8012,9 +8032,9 @@ dependencies = [
[[package]]
name = "tree-sitter-cpp"
version = "0.20.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a869e3c5cef4e5db4e9ab16a8dc84d73010e60ada14cdc60d2f6d8aed17779d"
checksum = "0dbedbf4066bfab725b3f9e2a21530507419a7d2f98621d3c13213502b734ec0"
dependencies = [
"cc",
"tree-sitter",
@ -8048,6 +8068,16 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-elm"
version = "5.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95236155fa1cd5fcf92123e7e6aa7b6e8c6756b54b5d39afd792a23bd6c9eb7b"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-embedded-template"
version = "0.20.0"
@ -8058,6 +8088,15 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-glsl"
version = "0.1.4"
source = "git+https://github.com/theHamsta/tree-sitter-glsl?rev=2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3#2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-go"
version = "0.19.1"
@ -8135,9 +8174,9 @@ dependencies = [
[[package]]
name = "tree-sitter-python"
version = "0.20.2"
version = "0.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda114f58048f5059dcf158aff691dffb8e113e6d2b50d94263fd68711975287"
checksum = "f47ebd9cac632764b2f4389b08517bf2ef895431dd163eb562e3d2062cc23a14"
dependencies = [
"cc",
"tree-sitter",
@ -8409,9 +8448,9 @@ dependencies = [
[[package]]
name = "urlencoding"
version = "2.1.2"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8db7427f936968176eaa7cdf81b7f98b980b18495ec28f1b5791ac3bfe3eea9"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "usvg"
@ -8571,6 +8610,7 @@ dependencies = [
"indoc",
"itertools",
"language",
"language_selector",
"log",
"nvim-rs",
"parking_lot 0.11.2",
@ -8580,6 +8620,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings",
"theme",
"tokio",
"util",
"workspace",
@ -8711,7 +8752,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
"wasm-bindgen-shared",
]
@ -8745,7 +8786,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@ -9328,9 +9369,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
checksum = "25b5872fa2e10bd067ae946f927e726d7d603eaeb6e02fa6a350e0722d2b8c11"
dependencies = [
"memchr",
]
@ -9460,7 +9501,7 @@ name = "xtask"
version = "0.1.0"
dependencies = [
"anyhow",
"clap 4.3.15",
"clap 4.3.19",
"schemars",
"serde_json",
"theme",
@ -9495,7 +9536,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.96.0"
version = "0.97.0"
dependencies = [
"activity_indicator",
"ai",
@ -9580,11 +9621,14 @@ dependencies = [
"tiny_http",
"toml",
"tree-sitter",
"tree-sitter-bash",
"tree-sitter-c",
"tree-sitter-cpp",
"tree-sitter-css",
"tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e)",
"tree-sitter-elm",
"tree-sitter-embedded-template",
"tree-sitter-glsl",
"tree-sitter-go",
"tree-sitter-heex",
"tree-sitter-html",
@ -9636,7 +9680,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.27",
]
[[package]]

View File

@ -107,11 +107,14 @@ tree-sitter = "0.20"
unindent = { version = "0.1.7" }
pretty_assertions = "1.3.0"
tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" }
tree-sitter-c = "0.20.1"
tree-sitter-cpp = "0.20.0"
tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" }
tree-sitter-elm = "5.6.4"
tree-sitter-embedded-template = "0.20.0"
tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" }
tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" }
tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" }

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 7.63H8" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="2" y="2" width="10" height="3" rx="0.5" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
<path d="M2.59375 5H11.4375L10.5581 11.5664C10.5248 11.8146 10.313 12 10.0625 12H3.93944C3.68812 12 3.47585 11.8134 3.44358 11.5642L2.59375 5Z" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 529 B

View File

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 11C5.46973 11 4.1268 11.1873 3.31522 11.3327C2.94367 11.3992 2.60079 11.0563 2.66733 10.6848C2.81266 9.8732 3 8.53027 3 7C3 5.8387 2.89211 4.78529 2.77656 3.99011C2.73589 3.71017 3.19546 3.51715 3.36119 3.7464C4.09612 4.76304 5.23301 6.23301 6.5 7.5C7.76699 8.76699 9.23696 9.90388 10.2536 10.6388C10.4828 10.8045 10.2898 11.2641 10.0099 11.2234C9.21472 11.1079 8.1613 11 7 11Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12.3594 3.35938C12.3594 3.35938 12.0146 2.9209 11.5312 2.4375C11.0479 1.9541 10.6406 1.64062 10.6406 1.64062" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
<path d="M11.3516 7.36803C11.3516 7.36803 10.7962 5.88996 9.48438 4.57812C8.17254 3.26629 6.64062 2.64155 6.64062 2.64155" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
<rect x="2.72266" y="8.73828" width="3.58525" height="2.72899" rx="0.5" transform="rotate(45 2.72266 8.73828)" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 10L12 10.8374C12 10.9431 11.9665 11.046 11.9044 11.1315L11.1498 12.1691C11.0557 12.2985 10.9054 12.375 10.7454 12.375L3.25461 12.375C3.09464 12.375 2.94433 12.2985 2.85024 12.1691L2.09563 11.1315C2.03348 11.046 2 10.9431 2 10.8374L2 2" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 12V10L7 11H12V12H2Z" fill="black"/>
<path d="M5.63246 2.04415C6.44914 2.31638 7 3.08066 7 3.94152V10.7306C7 11.0924 6.62757 11.3345 6.29693 11.1875L2.79693 9.63197C2.61637 9.55172 2.5 9.37266 2.5 9.17506V1.69371C2.5 1.35243 2.83435 1.11145 3.15811 1.21937L5.63246 2.04415Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.5 2C7.67157 2 7 2.67157 7 3.5V12C7 11.1954 10.2366 11.0382 11.5017 11.0075C11.7778 11.0008 12 10.7761 12 10.5V2.5C12 2.22386 11.7761 2 11.5 2H8.5Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5C12 10.7761 11.7761 11 11.5 11H2.5C2.22386 11 2 10.7761 2 10.5V4.88C2 4.60386 2.22386 4.38 2.5 4.38H4.4342C4.61518 4.38 4.78204 4.2822 4.87046 4.12428L5.35681 3.25572C5.44524 3.0978 5.61209 3 5.79308 3H8.20692C8.38791 3 8.55476 3.0978 8.64319 3.25572L9.12954 4.12428C9.21796 4.2822 9.38482 4.38 9.5658 4.38H11.5C11.7761 4.38 12 4.60386 12 4.88V10.5Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.005 9C7.90246 9 8.63 8.27246 8.63 7.375C8.63 6.47754 7.90246 5.75 7.005 5.75C6.10754 5.75 5.38 6.47754 5.38 7.375C5.38 8.27246 6.10754 9 7.005 9Z" fill="black" fill-opacity="0.33" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.63281 5.66406L6.99344 8.89844L10.3672 5.66406" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.35938 3.63281L5.125 6.99344L8.35938 10.3672" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5.64062 3.64062L8.89062 7.00125L5.64062 10.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 245 B

View File

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.63281 8.36719L6.99344 5.13281L10.3672 8.36719" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 246 B

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.375 2C2.5 2 2.5 3.5 2.5 4.5C2.5 5.5 2 6.50106 1 7C2 7.50106 2.5 8.5 2.5 9.5C2.5 10.5 2.5 12 4.375 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.63281 2C11.5078 2 11.5078 3.5 11.5078 4.5C11.5078 5.5 12.0078 6.50106 13.0078 7C12.0078 7.50106 11.5078 8.5 11.5078 9.5C11.5078 10.5 11.5078 12 9.63281 12" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 553 B

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="7" cy="4" rx="5" ry="2" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
<path d="M12 4V10C12 11.1046 9.76142 12 7 12C4.23858 12 2 11.1046 2 10V4" stroke="black" stroke-width="1.25"/>
<path d="M12 7C12 8.10457 9.76142 9 7 9C4.23858 9 2 8.10457 2 7" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5413 7.3125C12.6529 7.11913 12.6529 6.88088 12.5413 6.6875L10.0413 2.35738C9.92962 2.164 9.72329 2.04488 9.5 2.04488L4.5 2.04488C4.27671 2.04488 4.07038 2.164 3.95873 2.35738L1.45873 6.6875C1.34709 6.88088 1.34709 7.11913 1.45873 7.3125L3.95873 11.6426C4.07038 11.836 4.27671 11.9551 4.5 11.9551L9.5 11.9551C9.72329 11.9551 9.92962 11.836 10.0413 11.6426L12.5413 7.3125Z" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M6.75 4.14434C6.9047 4.05502 7.0953 4.05502 7.25 4.14434L9.34808 5.35566C9.50278 5.44498 9.59808 5.61004 9.59808 5.78868V8.21132C9.59808 8.38996 9.50278 8.55502 9.34808 8.64434L7.25 9.85566C7.0953 9.94498 6.9047 9.94498 6.75 9.85566L4.65192 8.64434C4.49722 8.55502 4.40192 8.38996 4.40192 8.21132L4.40192 5.78868C4.40192 5.61004 4.49722 5.44498 4.65192 5.35566L6.75 4.14434Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 949 B

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 4H10" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 7H12" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 10H8" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 381 B

View File

@ -0,0 +1,159 @@
{
"suffixes": {
"aac": "audio",
"bash": "terminal",
"bmp": "image",
"c": "code",
"conf": "settings",
"cpp": "code",
"cc": "code",
"css": "code",
"doc": "document",
"docx": "document",
"eslintrc": "eslint",
"eslintrc.js": "eslint",
"eslintrc.json": "eslint",
"flac": "audio",
"fish": "terminal",
"gitattributes": "vcs",
"gitignore": "vcs",
"gitmodules": "vcs",
"gif": "image",
"go": "code",
"h": "code",
"handlebars": "code",
"hbs": "template",
"htm": "template",
"html": "template",
"svelte": "template",
"hpp": "code",
"ico": "image",
"ini": "settings",
"java": "code",
"jpeg": "image",
"jpg": "image",
"js": "code",
"json": "storage",
"lock": "lock",
"log": "log",
"md": "document",
"mdx": "document",
"mp3": "audio",
"mp4": "video",
"ods": "document",
"odp": "document",
"odt": "document",
"ogg": "video",
"pdf": "document",
"php": "code",
"png": "image",
"ppt": "document",
"pptx": "document",
"prettierrc": "prettier",
"prettierignore": "prettier",
"ps1": "terminal",
"psd": "image",
"py": "code",
"rb": "code",
"rkt": "code",
"rs": "rust",
"rtf": "document",
"scm": "code",
"sh": "terminal",
"bashrc": "terminal",
"bash_profile": "terminal",
"bash_aliases": "terminal",
"bash_logout": "terminal",
"profile": "terminal",
"zshrc": "terminal",
"zshenv": "terminal",
"zsh_profile": "terminal",
"zsh_aliases": "terminal",
"zsh_histfile": "terminal",
"zlogin": "terminal",
"sql": "code",
"svg": "image",
"swift": "code",
"tiff": "image",
"toml": "toml",
"ts": "typescript",
"tsx": "code",
"txt": "document",
"wav": "audio",
"webm": "video",
"xls": "document",
"xlsx": "document",
"xml": "template",
"yaml": "settings",
"yml": "settings",
"zsh": "terminal"
},
"types": {
"audio": {
"icon": "icons/file_icons/audio.svg"
},
"code": {
"icon": "icons/file_icons/code.svg"
},
"collapsed_chevron": {
"icon": "icons/file_icons/chevron_right.svg"
},
"collapsed_folder": {
"icon": "icons/file_icons/folder.svg"
},
"default": {
"icon": "icons/file_icons/file.svg"
},
"document": {
"icon": "icons/file_icons/book.svg"
},
"eslint": {
"icon": "icons/file_icons/eslint.svg"
},
"expanded_chevron": {
"icon": "icons/file_icons/chevron_down.svg"
},
"expanded_folder": {
"icon": "icons/file_icons/folder_open.svg"
},
"image": {
"icon": "icons/file_icons/image.svg"
},
"lock": {
"icon": "icons/file_icons/lock.svg"
},
"log": {
"icon": "icons/file_icons/info.svg"
},
"prettier": {
"icon": "icons/file_icons/prettier.svg"
},
"rust": {
"icon": "icons/file_icons/rust.svg"
},
"settings": {
"icon": "icons/file_icons/settings.svg"
},
"storage": {
"icon": "icons/file_icons/database.svg"
},
"template": {
"icon": "icons/file_icons/html.svg"
},
"terminal": {
"icon": "icons/file_icons/terminal.svg"
},
"toml": {
"icon": "icons/file_icons/toml.svg"
},
"typescript": {
"icon": "icons/file_icons/typescript.svg"
},
"vcs": {
"icon": "icons/file_icons/git.svg"
},
"video": {
"icon": "icons/file_icons/video.svg"
}
}
}

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 5.125C2 4.84886 2.22386 4.625 2.5 4.625H11.5C11.7761 4.625 12 4.84886 12 5.125V11.125C12 11.4011 11.7761 11.625 11.5 11.625H2.5C2.22386 11.625 2 11.4011 2 11.125V5.125Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
<path d="M6.38197 2.375H2.5C2.22386 2.375 2 2.59886 2 2.875V4.375H8L7.27639 2.92779C7.107 2.589 6.76074 2.375 6.38197 2.375Z" fill="black"/>
<path d="M2 8V4.375M2 4.375V2.875C2 2.59886 2.22386 2.375 2.5 2.375H6.38197C6.76074 2.375 7.107 2.589 7.27639 2.92779L8 4.375H2Z" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@ -0,0 +1,5 @@
<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7 2.53125H2.21875V10.625L4.5 4.59375H7.96875L7 2.53125Z" fill="black"/>
<path d="M4.47293 4.94363C4.54554 4.74743 4.73263 4.61719 4.94184 4.61719H12.8755C13.2237 4.61719 13.4653 4.9642 13.3445 5.29074L11.1208 11.2986C11.0482 11.4948 10.8611 11.625 10.6519 11.625H2.71821C2.37002 11.625 2.12844 11.278 2.2493 10.9514L4.47293 4.94363Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
<path d="M8 4.4024L7.27505 2.93264C7.10664 2.59119 6.75894 2.375 6.37821 2.375H2.5C2.22386 2.375 2 2.59886 2 2.875V11.125C2 11.4011 2.22386 11.625 2.5 11.625H4.00781" stroke="black" stroke-width="1.25" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 745 B

View File

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="4" cy="10" r="2" stroke="black" stroke-width="1.25"/>
<circle cx="10" cy="4" r="2" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25"/>
<line x1="3.625" y1="2.625" x2="3.625" y2="7.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M10 6V6C10 8.20914 8.20914 10 6 10V10" stroke="black" stroke-width="1.25"/>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="10.2795" y1="2.63847" x2="7.74785" y2="11.0142" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<line x1="6.26624" y1="2.99597" x2="3.7346" y2="11.3717" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<line x1="3.15982" y1="5.3799" x2="11.9098" y2="5.3799" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<line x1="2.0983" y1="8.62407" x2="10.8483" y2="8.62407" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 571 B

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.15732 3.17108L5.84268 10.8289" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M4 5L2 7L4 9" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 9L12 7L10 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 423 B

View File

@ -0,0 +1,7 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.5 3C6.91421 3 7.25 2.66421 7.25 2.25C7.25 1.83579 6.91421 1.5 6.5 1.5C6.08579 1.5 5.75 1.83579 5.75 2.25C5.75 2.66421 6.08579 3 6.5 3Z" fill="black" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8L9 5L12 8H6Z" fill="black" fill-opacity="0.33"/>
<path d="M2 10L5 7L7.375 9.375" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8L7.5 6.5L9 5L10.5 6.5L12 8" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3.375 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H7.35938M9.64062 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10.125" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 866 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -0,0 +1,6 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="5" width="8" height="7" rx="0.5" stroke="black" stroke-width="1.25"/>
<path d="M4 4C4 2.89543 4.89543 2 6 2H8C9.10457 2 10 2.89543 10 4V5H4V4Z" stroke="black" stroke-opacity="0.66" stroke-width="1.25"/>
<circle cx="7" cy="8" r="1" fill="black"/>
<path d="M7 8V9.375" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 445 B

View File

@ -0,0 +1,8 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.03125 2.96875C2.03125 2.41647 2.47897 1.96875 3.03125 1.96875H5V12H3.03125C2.47897 12 2.03125 11.5523 2.03125 11V2.96875Z" fill="black" fill-opacity="0.33"/>
<rect x="2" y="2" width="10" height="10" rx="0.5" stroke="black" stroke-width="1.25"/>
<path d="M9.5 5L7.5 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 7H7.5" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.5 9H7.5" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 2V13" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.62677 3.88472L6.99983 6.78517M1.62677 3.88472L1.63137 9.90006L7.00442 12.8005M1.62677 3.88472L4.31117 2.54211M6.99983 6.78517L7.00442 12.8005M6.99983 6.78517L9.68414 5.33084M7.00442 12.8005L12.373 9.89186L12.3684 3.87652M4.31117 2.54211L6.99556 1.1995L12.3684 3.87652M4.31117 2.54211L9.68414 5.33084M12.3684 3.87652L9.68414 5.33084" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.03125 12.5625V6.78125L1.5625 3.9375V9.75L7.03125 12.5625Z" fill="black" fill-opacity="0.33"/>
</svg>

After

Width:  |  Height:  |  Size: 638 B

View File

@ -0,0 +1,12 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 2.86328H8.51563" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M11 2.86328L12 2.86328" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
<path d="M9.64062 5.6263L12 5.6263" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
<path d="M4.79688 5.6263L7.15625 5.6263" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 5.6263L2.35937 5.6263" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
<path d="M7.15625 8.3737L12 8.3737" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 8.3737L4.64062 8.3737" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
<path d="M2 11.1094H3.54687" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M5.97656 11.1094H8.35938" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M10.8203 11.1094L12 11.1094" stroke="black" stroke-opacity="0.33" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.27935 9.98207C4.32063 9.4038 3.9204 8.89049 3.35998 8.80276L2.60081 8.68387C2.37979 8.64945 2.20167 8.48001 2.15225 8.25614L2.01378 7.63511C1.96382 7.41235 2.05233 7.1807 2.23696 7.05125L2.8631 6.61242C3.33337 6.28297 3.47456 5.6369 3.18621 5.13364L2.79467 4.45092C2.68118 4.25261 2.69801 4.00374 2.83757 3.82321L3.22314 3.32436C3.3627 3.14438 3.59621 3.06994 3.81071 3.13772L4.57531 3.37769C5.11944 3.54879 5.70048 3.26159 5.90683 2.71886L6.1811 1.99782C6.26255 1.78395 6.46345 1.64285 6.68772 1.6423L7.31007 1.64063C7.53434 1.64007 7.73579 1.78006 7.81834 1.99337L8.09965 2.72275C8.30821 3.26214 8.88655 3.54712 9.42903 3.37714L10.1632 3.14716C10.3772 3.07994 10.6096 3.15382 10.7492 3.3327L11.1374 3.83099" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.76988 10.5933C7.76988 10.6595 7.8236 10.7133 7.88988 10.7133H7.97588C8.32602 10.7133 8.60988 10.9971 8.60988 11.3472V11.3472C8.60988 11.6974 8.32602 11.9812 7.97588 11.9812H6.05587C5.70573 11.9812 5.42188 11.6974 5.42188 11.3472V11.3472C5.42188 10.9971 5.70573 10.7133 6.05587 10.7133H6.14188C6.20815 10.7133 6.26188 10.6595 6.26188 10.5933V6.66925C6.26188 6.60298 6.20815 6.54925 6.14188 6.54925H6.05588C5.70573 6.54925 5.42188 6.2654 5.42188 5.91525V5.91525C5.42188 5.5651 5.70573 5.28125 6.05588 5.28125H8.89988C10.0518 5.28125 11.8619 5.71487 11.8619 7.15185C11.8619 7.67078 11.7284 8.10362 11.4642 8.45348C11.1981 8.79765 10.8458 9.05637 10.4056 9.22931V9.22931C10.3782 9.24007 10.3673 9.27304 10.3829 9.29801L11.2163 10.6342C11.247 10.6834 11.3008 10.7133 11.3588 10.7133H11.7319C12.082 10.7133 12.3659 10.9971 12.3659 11.3472V11.3472C12.3659 11.6974 12.082 11.9812 11.7319 11.9812H10.5637C10.4955 11.9812 10.432 11.9465 10.3952 11.889L8.96523 9.65406C8.92847 9.59661 8.86496 9.56185 8.79676 9.56185H7.96988C7.85942 9.56185 7.76988 9.65139 7.76988 9.76185V10.5933ZM8.61188 6.54925C9.02963 6.54925 10.125 6.54925 10.2339 7.18785C10.2975 7.56123 10.1181 7.86557 9.88118 8.07715C9.64227 8.29046 9.20527 8.38985 8.58788 8.38985H7.86988C7.81465 8.38985 7.76988 8.34508 7.76988 8.28985V6.64925C7.76988 6.59402 7.81465 6.54925 7.86988 6.54925H8.61188Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.60081 8.94324L3.35998 9.06214C3.9204 9.14986 4.32063 9.66317 4.27935 10.2414L4.22342 11.0252C4.20713 11.2536 4.32877 11.4686 4.53024 11.568L5.09174 11.8446C5.29321 11.9441 5.53379 11.9068 5.69834 11.7519L6.26255 11.2186C6.67855 10.8253 7.32041 10.8253 7.7369 11.2186L8.3011 11.7519C8.46565 11.9074 8.70572 11.9441 8.90772 11.8446L9.47027 11.5674C9.67124 11.4686 9.79234 11.2541 9.77607 11.0264L9.72007 10.2414C9.67883 9.66317 10.079 9.14986 10.6394 9.06214L11.3986 8.94324C11.6197 8.90883 11.7978 8.73938 11.8477 8.51607L11.9862 7.89504C12.0362 7.67172 11.9477 7.44007 11.763 7.31117L11.1293 6.86731C10.6617 6.53959 10.5189 5.89966 10.8013 5.3969L11.1841 4.71586C11.2954 4.51754 11.277 4.26923 11.1374 4.09036L10.7492 3.59207C10.6096 3.41319 10.3772 3.33932 10.1632 3.40653L9.42903 3.63651C8.88655 3.80649 8.30821 3.52152 8.09965 2.98213L7.81834 2.25275C7.73579 2.03944 7.53434 1.89945 7.31007 1.9L6.68772 1.90167C6.46345 1.90222 6.26255 2.04333 6.1811 2.25719L5.90683 2.97824C5.70048 3.52097 5.11944 3.80816 4.57531 3.63706L3.81071 3.39709C3.59621 3.32932 3.3627 3.40375 3.22314 3.58374L2.83757 4.08258C2.69801 4.26312 2.68118 4.51199 2.79467 4.7103L3.18621 5.39302C3.47456 5.89628 3.33337 6.54235 2.8631 6.87179L2.23696 7.31062C2.05233 7.44007 1.96382 7.67173 2.01378 7.89448L2.15225 8.51552C2.20167 8.73938 2.37979 8.90883 2.60081 8.94324Z" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8.14913 5.85093L8.14909 5.85089C7.51453 5.21637 6.48549 5.21637 5.85092 5.85089L5.85089 5.85092C5.21637 6.48549 5.21637 7.51453 5.85089 8.14909L5.85093 8.14913C6.48549 8.78362 7.51452 8.78362 8.14908 8.14913L8.14913 8.14908C8.78362 7.51452 8.78362 6.48549 8.14913 5.85093Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.65625 2.5C1.65625 2.22386 1.88011 2 2.15625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V11.5C12.3438 11.7761 12.1199 12 11.8437 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V2.5Z" stroke="black" stroke-width="1.25"/>
<path d="M4.375 9L6.375 7L4.375 5" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.625 9L9.90625 9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 549 B

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 5H9" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M7 5L7 10" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M4 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H4M10 2H11.5C11.7761 2 12 2.22386 12 2.5V11.5C12 11.7761 11.7761 12 11.5 12H10" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@ -0,0 +1,5 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 4.375V2.5C12 2.22386 11.7761 2 11.5 2H2.5C2.22386 2 2 2.22386 2 2.5V11.5C2 11.7761 2.22386 12 2.5 12H3.375" stroke="black" stroke-opacity="0.66" stroke-width="1.25" stroke-linecap="round"/>
<path d="M10.6836 7.82805C10.7933 7.65392 10.9823 7.57377 11.174 7.57377C11.2904 7.57377 11.4019 7.59384 11.5092 7.62792C11.8324 7.73069 12.2148 7.63925 12.3392 7.32368L12.3773 7.22707C12.4703 6.99131 12.3823 6.71761 12.1522 6.61154C11.8328 6.46436 11.4984 6.375 11.1262 6.375C9.87708 6.375 8.91935 7.60671 9.4239 8.84869C9.54205 9.13951 9.74219 9.36166 9.9515 9.54337C10.1061 9.6776 10.2858 9.80516 10.4475 9.92002C10.4972 9.95529 10.5452 9.98936 10.5903 10.0221C11.0283 10.34 11.2526 10.5876 11.2526 10.9466C11.2526 11.1518 11.1622 11.3133 11.016 11.4128C10.8777 11.5071 10.7055 11.5357 10.5454 11.5222C10.3931 11.5093 10.2529 11.4717 10.1214 11.4196C9.81633 11.2989 9.45533 11.4015 9.33641 11.7073L9.2814 11.8487C9.19162 12.0796 9.2749 12.3463 9.49799 12.4539C10.0894 12.7391 10.7377 12.8279 11.3915 12.5872C12.0569 12.3423 12.595 11.7708 12.595 10.9068C12.595 10.1301 12.1336 9.69583 11.6966 9.36109C11.606 9.29163 11.5259 9.23292 11.4493 9.17682C11.3259 9.08638 11.1964 8.99109 11.0734 8.88536C10.8937 8.73082 10.7518 8.57274 10.6595 8.38613C10.5746 8.21464 10.5815 7.99013 10.6836 7.82805Z" fill="black"/>
<path d="M6.98644 7.70936H7.69396C7.98162 7.70936 8.21481 7.47617 8.21481 7.18851V7.02346C8.21481 6.73581 7.98162 6.50261 7.69396 6.50261H4.96848C4.68082 6.50261 4.44763 6.73581 4.44763 7.02346V7.18851C4.44763 7.47617 4.68082 7.70936 4.96848 7.70936H5.676V12.102C5.676 12.3896 5.90919 12.6228 6.19685 12.6228H6.46559C6.75325 12.6228 6.98644 12.3896 6.98644 12.102V7.70936Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.65625 2H11.8437C12.1199 2 12.3438 2.22386 12.3438 2.5V9.34375M12.3438 12H2.15625C1.88011 12 1.65625 11.7761 1.65625 11.5V4.65625" stroke="black" stroke-width="1.25" stroke-linecap="round"/>
<path d="M9 7.01562L5.65624 9.3125L5.65624 4.6875L9 7.01562Z" fill="black" fill-opacity="0.33" stroke="black" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -195,8 +195,8 @@
{
"context": "Editor && mode == auto_height",
"bindings": {
"shift-enter": "editor::Newline",
"cmd-shift-enter": "editor::NewlineBelow"
"ctrl-enter": "editor::Newline",
"ctrl-shift-enter": "editor::NewlineBelow"
}
},
{
@ -406,6 +406,7 @@
"cmd-b": "workspace::ToggleLeftDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"alt-cmd-y": "workspace::CloseAllDocks",
"cmd-shift-f": "workspace::NewSearch",
"cmd-k cmd-t": "theme_selector::Toggle",
"cmd-k cmd-s": "zed::OpenKeymap",
@ -446,8 +447,22 @@
},
{
"bindings": {
"cmd-k cmd-left": "workspace::ActivatePreviousPane",
"cmd-k cmd-right": "workspace::ActivateNextPane"
"cmd-k cmd-left": [
"workspace::ActivatePaneInDirection",
"Left"
],
"cmd-k cmd-right": [
"workspace::ActivatePaneInDirection",
"Right"
],
"cmd-k cmd-up": [
"workspace::ActivatePaneInDirection",
"Up"
],
"cmd-k cmd-down": [
"workspace::ActivatePaneInDirection",
"Down"
]
}
},
// Bindings from Atom
@ -513,8 +528,11 @@
"cmd-alt-c": "project_panel::CopyPath",
"alt-cmd-shift-c": "project_panel::CopyRelativePath",
"f2": "project_panel::Rename",
"enter": "project_panel::Rename",
"space": "project_panel::Open",
"backspace": "project_panel::Delete",
"alt-cmd-r": "project_panel::RevealInFinder"
"alt-cmd-r": "project_panel::RevealInFinder",
"alt-shift-f": "project_panel::NewSearchInDirectory"
}
},
{

View File

@ -2,12 +2,6 @@
{
"context": "Editor && VimControl && !VimWaiting && !menu",
"bindings": {
"g": [
"vim::PushOperator",
{
"Namespace": "G"
}
],
"i": [
"vim::PushOperator",
{
@ -30,6 +24,8 @@
"j": "vim::Down",
"down": "vim::Down",
"enter": "vim::NextLineStart",
"tab": "vim::Tab",
"shift-tab": "vim::Tab",
"k": "vim::Up",
"up": "vim::Up",
"l": "vim::Right",
@ -60,6 +56,8 @@
"ignorePunctuation": true
}
],
"n": "search::SelectNextMatch",
"shift-n": "search::SelectPrevMatch",
"%": "vim::Matching",
"f": [
"vim::PushOperator",
@ -103,7 +101,35 @@
"vim::SwitchMode",
"Normal"
],
"*": "vim::MoveToNext",
"#": "vim::MoveToPrev",
"0": "vim::StartOfLine", // When no number operator present, use start of line motion
// "g" commands
"g g": "vim::StartOfDocument",
"g h": "editor::Hover",
"g t": "pane::ActivateNextItem",
"g shift-t": "pane::ActivatePrevItem",
"g d": "editor::GoToDefinition",
"g shift-d": "editor::GoToTypeDefinition",
"g .": "editor::ToggleCodeActions", // zed specific
"g shift-a": "editor::FindAllReferences", // zed specific
"g *": [
"vim::MoveToNext",
{
"partialWord": true
}
],
"g #": [
"vim::MoveToPrev",
{
"partialWord": true
}
],
// z commands
"z t": "editor::ScrollCursorTop",
"z z": "editor::ScrollCursorCenter",
"z b": "editor::ScrollCursorBottom",
// Count support
"1": [
"vim::Number",
1
@ -139,7 +165,75 @@
"9": [
"vim::Number",
9
]
],
// window related commands (ctrl-w X)
"ctrl-w left": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w right": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w up": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w down": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w h": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w l": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w k": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w j": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w ctrl-h": [
"workspace::ActivatePaneInDirection",
"Left"
],
"ctrl-w ctrl-l": [
"workspace::ActivatePaneInDirection",
"Right"
],
"ctrl-w ctrl-k": [
"workspace::ActivatePaneInDirection",
"Up"
],
"ctrl-w ctrl-j": [
"workspace::ActivatePaneInDirection",
"Down"
],
"ctrl-w g t": "pane::ActivateNextItem",
"ctrl-w ctrl-g t": "pane::ActivateNextItem",
"ctrl-w g shift-t": "pane::ActivatePrevItem",
"ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem",
"ctrl-w w": "workspace::ActivateNextPane",
"ctrl-w ctrl-w": "workspace::ActivateNextPane",
"ctrl-w p": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-p": "workspace::ActivatePreviousPane",
"ctrl-w shift-w": "workspace::ActivatePreviousPane",
"ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane",
"ctrl-w v": "pane::SplitLeft",
"ctrl-w ctrl-v": "pane::SplitLeft",
"ctrl-w s": "pane::SplitUp",
"ctrl-w shift-s": "pane::SplitUp",
"ctrl-w ctrl-s": "pane::SplitUp",
"ctrl-w c": "pane::CloseAllItems",
"ctrl-w ctrl-c": "pane::CloseAllItems",
"ctrl-w q": "pane::CloseAllItems",
"ctrl-w ctrl-q": "pane::CloseAllItems"
}
},
{
@ -160,12 +254,6 @@
"vim::PushOperator",
"Yank"
],
"z": [
"vim::PushOperator",
{
"Namespace": "Z"
}
],
"i": [
"vim::SwitchMode",
"Insert"
@ -197,10 +285,18 @@
"p": "vim::Paste",
"u": "editor::Undo",
"ctrl-r": "editor::Redo",
"/": [
"buffer_search::Deploy",
"/": "vim::Search",
"?": [
"vim::Search",
{
"focus": true
"backwards": true
}
],
";": "vim::RepeatFind",
",": [
"vim::RepeatFind",
{
"backwards": true
}
],
"ctrl-f": "vim::PageDown",
@ -231,20 +327,11 @@
]
}
},
{
"context": "Editor && vim_operator == g",
"bindings": {
"g": "vim::StartOfDocument",
"h": "editor::Hover",
"t": "pane::ActivateNextItem",
"shift-t": "pane::ActivatePrevItem",
"d": "editor::GoToDefinition"
}
},
{
"context": "Editor && vim_operator == c",
"bindings": {
"c": "vim::CurrentLine"
"c": "vim::CurrentLine",
"d": "editor::Rename" // zed specific
}
},
{
@ -259,14 +346,6 @@
"y": "vim::CurrentLine"
}
},
{
"context": "Editor && vim_operator == z",
"bindings": {
"t": "editor::ScrollCursorTop",
"z": "editor::ScrollCursorCenter",
"b": "editor::ScrollCursorBottom",
}
},
{
"context": "Editor && VimObject",
"bindings": {
@ -310,8 +389,8 @@
"vim::SwitchMode",
"Normal"
],
"> >": "editor::Indent",
"< <": "editor::Outdent"
">": "editor::Indent",
"<": "editor::Outdent"
}
},
{
@ -319,7 +398,7 @@
"bindings": {
"escape": "vim::NormalBefore",
"ctrl-c": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore",
"ctrl-[": "vim::NormalBefore"
}
},
{
@ -336,5 +415,12 @@
"Normal"
]
}
},
{
"context": "BufferSearchBar > VimEnabled",
"bindings": {
"enter": "vim::SearchSubmit",
"escape": "buffer_search::Dismiss"
}
}
]

View File

@ -50,6 +50,13 @@
// Whether to pop the completions menu while typing in an editor without
// explicitly requesting it.
"show_completions_on_input": true,
// Whether to show wrap guides in the editor. Setting this to true will
// show a guide at the 'preferred_line_length' value if softwrap is set to
// 'preferred_line_length', and will show any additional guides as specified
// by the 'wrap_guides' setting.
"show_wrap_guides": true,
// Character counts at which to show wrap guides in the editor.
"wrap_guides": [],
// Whether to use additional LSP queries to format (and amend) the code after
// every "trigger" symbol input, defined by LSP server capabilities.
"use_on_type_format": true,
@ -66,6 +73,11 @@
// 3. Draw all invisible symbols:
// "all"
"show_whitespaces": "selection",
// Settings related to calls in Zed
"calls": {
// Join calls with the microphone muted by default
"mute_on_join": true
},
// Scrollbar related settings
"scrollbar": {
// When to show the scrollbar in the editor.
@ -97,12 +109,18 @@
"show_other_hints": true
},
"project_panel": {
// Whether to show the git status in the project panel.
"git_status": true,
// Default width of the project panel.
"default_width": 240,
// Where to dock project panel. Can be 'left' or 'right'.
"dock": "left",
// Default width of the project panel.
"default_width": 240
// Whether to show file icons in the project panel.
"file_icons": true,
// Whether to show folder icons or chevrons for directories in the project panel.
"folder_icons": true,
// Whether to show the git status in the project panel.
"git_status": true,
// Amount of indentation for nested items.
"indent_size": 20
},
"assistant": {
// Where to dock the assistant. Can be 'left', 'right' or 'bottom'.
@ -196,9 +214,7 @@
"copilot": {
// The set of glob patterns for which copilot should be disabled
// in any matching file.
"disabled_globs": [
".env"
]
"disabled_globs": [".env"]
},
// Settings specific to journaling
"journal": {
@ -347,12 +363,6 @@
// LSP Specific settings.
"lsp": {
// Specify the LSP name as a key here.
// As of 8/10/22, supported LSPs are:
// pyright
// gopls
// rust-analyzer
// typescript-language-server
// vscode-json-languageserver
// "rust-analyzer": {
// //These initialization options are merged into Zed's defaults
// "initialization_options": {

View File

@ -298,12 +298,22 @@ impl AssistantPanel {
}
fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
let mut propagate_action = true;
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
return;
}
search_bar.update(cx, |search_bar, cx| {
if search_bar.show(cx) {
search_bar.search_suggested(cx);
if action.focus {
search_bar.select_query(cx);
cx.focus_self();
}
propagate_action = false
}
});
}
if propagate_action {
cx.propagate_action();
}
cx.propagate_action();
}
fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
@ -320,13 +330,13 @@ impl AssistantPanel {
fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx));
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
}
}
fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx));
search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
}
}

View File

@ -36,6 +36,10 @@ anyhow.workspace = true
async-broadcast = "0.4"
futures.workspace = true
postage.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_derive.workspace = true
[dev-dependencies]
client = { path = "../client", features = ["test-support"] }

View File

@ -1,9 +1,11 @@
pub mod call_settings;
pub mod participant;
pub mod room;
use std::sync::Arc;
use anyhow::{anyhow, Result};
use call_settings::CallSettings;
use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore};
use collections::HashSet;
use futures::{future::Shared, FutureExt};
@ -19,6 +21,8 @@ pub use participant::ParticipantLocation;
pub use room::Room;
pub fn init(client: Arc<Client>, user_store: ModelHandle<UserStore>, cx: &mut AppContext) {
settings::register::<CallSettings>(cx);
let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx));
cx.set_global(active_call);
}
@ -280,21 +284,6 @@ impl ActiveCall {
}
}
pub fn toggle_screen_sharing(&self, cx: &mut AppContext) {
if let Some(room) = self.room().cloned() {
let toggle_screen_sharing = room.update(cx, |room, cx| {
if room.is_screen_sharing() {
self.report_call_event("disable screen share", cx);
Task::ready(room.unshare_screen(cx))
} else {
self.report_call_event("enable screen share", cx);
room.share_screen(cx)
}
});
toggle_screen_sharing.detach_and_log_err(cx);
}
}
pub fn share_project(
&mut self,
project: ModelHandle<Project>,

View File

@ -0,0 +1,27 @@
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Setting;
#[derive(Deserialize, Debug)]
pub struct CallSettings {
pub mute_on_join: bool,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct CallSettingsContent {
pub mute_on_join: Option<bool>,
}
impl Setting for CallSettings {
const KEY: Option<&'static str> = Some("calls");
type FileContent = CallSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

View File

@ -1,4 +1,5 @@
use crate::{
call_settings::CallSettings,
participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack},
IncomingCall,
};
@ -153,8 +154,10 @@ impl Room {
cx.spawn(|this, mut cx| async move {
connect.await?;
this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?;
if !cx.read(|cx| settings::get::<CallSettings>(cx).mute_on_join) {
this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?;
}
anyhow::Ok(())
})
@ -656,7 +659,7 @@ impl Room {
peer_id,
projects: participant.projects,
location,
muted: false,
muted: true,
speaking: false,
video_tracks: Default::default(),
audio_tracks: Default::default(),
@ -670,6 +673,10 @@ impl Room {
live_kit.room.remote_video_tracks(&user.id.to_string());
let audio_tracks =
live_kit.room.remote_audio_tracks(&user.id.to_string());
let publications = live_kit
.room
.remote_audio_track_publications(&user.id.to_string());
for track in video_tracks {
this.remote_video_track_updated(
RemoteVideoTrackUpdate::Subscribed(track),
@ -677,9 +684,15 @@ impl Room {
)
.log_err();
}
for track in audio_tracks {
for (track, publication) in
audio_tracks.iter().zip(publications.iter())
{
this.remote_audio_track_updated(
RemoteAudioTrackUpdate::Subscribed(track),
RemoteAudioTrackUpdate::Subscribed(
track.clone(),
publication.clone(),
),
cx,
)
.log_err();
@ -819,8 +832,8 @@ impl Room {
cx.notify();
}
RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => {
let mut found = false;
for participant in &mut self.remote_participants.values_mut() {
let mut found = false;
for track in participant.audio_tracks.values() {
if track.sid() == track_id {
found = true;
@ -832,16 +845,20 @@ impl Room {
break;
}
}
cx.notify();
}
RemoteAudioTrackUpdate::Subscribed(track) => {
RemoteAudioTrackUpdate::Subscribed(track, publication) => {
let user_id = track.publisher_id().parse()?;
let track_id = track.sid().to_string();
let participant = self
.remote_participants
.get_mut(&user_id)
.ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?;
participant.audio_tracks.insert(track_id.clone(), track);
participant.muted = publication.is_muted();
cx.emit(Event::RemoteAudioTracksChanged {
participant_id: participant.peer_id,
});
@ -1053,7 +1070,7 @@ impl Room {
self.live_kit
.as_ref()
.and_then(|live_kit| match &live_kit.microphone_track {
LocalTrack::None => None,
LocalTrack::None => Some(true),
LocalTrack::Pending { muted, .. } => Some(*muted),
LocalTrack::Published { muted, .. } => Some(*muted),
})
@ -1070,6 +1087,7 @@ impl Room {
self.live_kit.as_ref().map(|live_kit| live_kit.deafened)
}
#[track_caller]
pub fn share_microphone(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
if self.status.is_offline() {
return Task::ready(Err(anyhow!("room is offline")));
@ -1244,6 +1262,10 @@ impl Room {
pub fn toggle_mute(&mut self, cx: &mut ModelContext<Self>) -> Result<Task<Result<()>>> {
let should_mute = !self.is_muted();
if let Some(live_kit) = self.live_kit.as_mut() {
if matches!(live_kit.microphone_track, LocalTrack::None) {
return Ok(self.share_microphone(cx));
}
let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
live_kit.muted_by_user = should_mute;

View File

@ -40,6 +40,7 @@ lazy_static! {
struct ClickhouseEventRequestBody {
token: &'static str,
installation_id: Option<Arc<str>>,
is_staff: Option<bool>,
app_version: Option<Arc<str>>,
os_name: &'static str,
os_version: Option<Arc<str>>,
@ -224,6 +225,7 @@ impl Telemetry {
&ClickhouseEventRequestBody {
token: ZED_SECRET_CLIENT_TOKEN,
installation_id: state.installation_id.clone(),
is_staff: state.is_staff.clone(),
app_version: state.app_version.clone(),
os_name: state.os_name,
os_version: state.os_version.clone(),

View File

@ -652,10 +652,10 @@ impl CollabTitlebarItem {
let is_muted = room.read(cx).is_muted();
if is_muted {
icon = "icons/radix/mic-mute.svg";
tooltip = "Unmute microphone\nRight click for options";
tooltip = "Unmute microphone";
} else {
icon = "icons/radix/mic.svg";
tooltip = "Mute microphone\nRight click for options";
tooltip = "Mute microphone";
}
let titlebar = &theme.titlebar;
@ -705,10 +705,10 @@ impl CollabTitlebarItem {
let is_deafened = room.read(cx).is_deafened().unwrap_or(false);
if is_deafened {
icon = "icons/radix/speaker-off.svg";
tooltip = "Unmute speakers\nRight click for options";
tooltip = "Unmute speakers";
} else {
icon = "icons/radix/speaker-loud.svg";
tooltip = "Mute speakers\nRight click for options";
tooltip = "Mute speakers";
}
let titlebar = &theme.titlebar;

View File

@ -18,13 +18,7 @@ use workspace::AppState;
actions!(
collab,
[
ToggleScreenSharing,
ToggleMute,
ToggleDeafen,
LeaveCall,
ShareMicrophone
]
[ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall]
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@ -40,7 +34,6 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
cx.add_global_action(toggle_screen_sharing);
cx.add_global_action(toggle_mute);
cx.add_global_action(toggle_deafen);
cx.add_global_action(share_microphone);
}
pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
@ -71,10 +64,24 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
}
pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, Room::toggle_mute)
.map(|task| task.detach_and_log_err(cx))
.log_err();
let call = ActiveCall::global(cx).read(cx);
if let Some(room) = call.room().cloned() {
let client = call.client();
room.update(cx, |room, cx| {
if room.is_muted() {
ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx);
} else {
ActiveCall::report_call_event_for_room(
"disable microphone",
room.id(),
&client,
cx,
);
}
room.toggle_mute(cx)
})
.map(|task| task.detach_and_log_err(cx))
.log_err();
}
}
@ -85,10 +92,3 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
.log_err();
}
}
pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) {
if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() {
room.update(cx, Room::share_microphone)
.detach_and_log_err(cx)
}
}

View File

@ -10,7 +10,6 @@ doctest = false
[features]
test-support = [
"rand",
"copilot/test-support",
"text/test-support",
"language/test-support",
@ -62,8 +61,8 @@ serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
rand.workspace = true
rand = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }

View File

@ -74,6 +74,7 @@ pub use multi_buffer::{
};
use ordered_float::OrderedFloat;
use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
use rand::{seq::SliceRandom, thread_rng};
use scroll::{
autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
};
@ -226,6 +227,10 @@ actions!(
MoveLineUp,
MoveLineDown,
JoinLines,
SortLinesCaseSensitive,
SortLinesCaseInsensitive,
ReverseLines,
ShuffleLines,
Transpose,
Cut,
Copy,
@ -344,6 +349,10 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::outdent);
cx.add_action(Editor::delete_line);
cx.add_action(Editor::join_lines);
cx.add_action(Editor::sort_lines_case_sensitive);
cx.add_action(Editor::sort_lines_case_insensitive);
cx.add_action(Editor::reverse_lines);
cx.add_action(Editor::shuffle_lines);
cx.add_action(Editor::delete_to_previous_word_start);
cx.add_action(Editor::delete_to_previous_subword_start);
cx.add_action(Editor::delete_to_next_word_end);
@ -549,6 +558,7 @@ pub struct Editor {
pending_rename: Option<RenameState>,
searchable: bool,
cursor_shape: CursorShape,
collapse_matches: bool,
workspace: Option<(WeakViewHandle<Workspace>, i64)>,
keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
input_enabled: bool,
@ -562,6 +572,7 @@ pub struct Editor {
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
pixel_position_of_newest_cursor: Option<Vector2F>,
}
pub struct EditorSnapshot {
@ -1381,6 +1392,7 @@ impl Editor {
searchable: true,
override_text_style: None,
cursor_shape: Default::default(),
collapse_matches: false,
workspace: None,
keymap_context_layers: Default::default(),
input_enabled: true,
@ -1392,6 +1404,7 @@ impl Editor {
copilot_state: Default::default(),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
pixel_position_of_newest_cursor: None,
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@ -1520,6 +1533,17 @@ impl Editor {
cx.notify();
}
pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
self.collapse_matches = collapse_matches;
}
fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
if self.collapse_matches {
return range.start..range.start;
}
range.clone()
}
pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
if self.display_map.read(cx).clip_at_line_ends != clip {
self.display_map
@ -2659,11 +2683,16 @@ impl Editor {
InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
};
self.inlay_hint_cache.refresh_inlay_hints(
if let Some(InlaySplice {
to_remove,
to_insert,
}) = self.inlay_hint_cache.spawn_hint_refresh(
self.excerpt_visible_offsets(required_languages.as_ref(), cx),
invalidate_cache,
cx,
)
) {
self.splice_inlay_hints(to_remove, to_insert, cx);
}
}
fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
@ -4185,6 +4214,96 @@ impl Editor {
});
}
pub fn sort_lines_case_sensitive(
&mut self,
_: &SortLinesCaseSensitive,
cx: &mut ViewContext<Self>,
) {
self.manipulate_lines(cx, |text| text.sort())
}
pub fn sort_lines_case_insensitive(
&mut self,
_: &SortLinesCaseInsensitive,
cx: &mut ViewContext<Self>,
) {
self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase()))
}
pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
self.manipulate_lines(cx, |lines| lines.reverse())
}
pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext<Self>) {
self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng()))
}
fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
where
Fn: FnMut(&mut [&str]),
{
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = self.buffer.read(cx).snapshot(cx);
let mut edits = Vec::new();
let selections = self.selections.all::<Point>(cx);
let mut selections = selections.iter().peekable();
let mut contiguous_row_selections = Vec::new();
let mut new_selections = Vec::new();
while let Some(selection) = selections.next() {
let (start_row, end_row) = consume_contiguous_rows(
&mut contiguous_row_selections,
selection,
&display_map,
&mut selections,
);
let start_point = Point::new(start_row, 0);
let end_point = Point::new(end_row - 1, buffer.line_len(end_row - 1));
let text = buffer
.text_for_range(start_point..end_point)
.collect::<String>();
let mut text = text.split("\n").collect_vec();
let text_len = text.len();
callback(&mut text);
// This is a current limitation with selections.
// If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections.
debug_assert!(
text.len() == text_len,
"callback should not change the number of lines"
);
edits.push((start_point..end_point, text.join("\n")));
let start_anchor = buffer.anchor_after(start_point);
let end_anchor = buffer.anchor_before(end_point);
// Make selection and push
new_selections.push(Selection {
id: selection.id,
start: start_anchor.to_offset(&buffer),
end: end_anchor.to_offset(&buffer),
goal: SelectionGoal::None,
reversed: selection.reversed,
});
}
self.transact(cx, |this, cx| {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
this.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select(new_selections);
});
this.request_autoscroll(Autoscroll::fit(), cx);
});
}
pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let buffer = &display_map.buffer_snapshot;
@ -5278,7 +5397,7 @@ impl Editor {
pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
let end = self.buffer.read(cx).read(cx).len();
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
self.change_selections(None, cx, |s| {
s.select_ranges(vec![0..end]);
});
}
@ -6256,6 +6375,7 @@ impl Editor {
.to_offset(definition.target.buffer.read(cx));
if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() {
let range = self.range_for_match(&range);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range]);
});
@ -6272,6 +6392,7 @@ impl Editor {
// When selecting a definition in a different buffer, disable the nav history
// to avoid creating a history entry at the previous cursor location.
pane.update(cx, |pane, _| pane.disable_history());
let range = target_editor.range_for_match(&range);
target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([range]);
});
@ -7064,6 +7185,20 @@ impl Editor {
.text()
}
pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
let mut wrap_guides = smallvec::smallvec![];
let settings = self.buffer.read(cx).settings_at(0, cx);
if settings.show_wrap_guides {
if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
wrap_guides.push((soft_wrap as usize, true));
}
wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
}
wrap_guides
}
pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
let settings = self.buffer.read(cx).settings_at(0, cx);
let mode = self

View File

@ -2500,6 +2500,156 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
});
}
#[gpui::test]
async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
// Test sort_lines_case_insensitive()
cx.set_state(indoc! {"
«z
y
x
Z
Y
»
"});
cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
cx.assert_editor_state(indoc! {"
«x
X
y
Y
z
»
"});
// Test reverse_lines()
cx.set_state(indoc! {"
«5
4
3
2
1ˇ»
"});
cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
cx.assert_editor_state(indoc! {"
«1
2
3
4
5ˇ»
"});
// Skip testing shuffle_line()
// From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
// Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
// Don't manipulate when cursor is on single line, but expand the selection
cx.set_state(indoc! {"
ddˇdd
ccc
bb
a
"});
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
cx.assert_editor_state(indoc! {"
«ddddˇ»
ccc
bb
a
"});
// Basic manipulate case
// Start selection moves to column 0
// End of selection shrinks to fit shorter line
cx.set_state(indoc! {"
dd«d
ccc
bb
aaaaaˇ»
"});
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
cx.assert_editor_state(indoc! {"
«aaaaa
bb
ccc
dddˇ»
"});
// Manipulate case with newlines
cx.set_state(indoc! {"
dd«d
ccc
bb
aaaaa
ˇ»
"});
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
cx.assert_editor_state(indoc! {"
«
aaaaa
bb
ccc
dddˇ»
"});
}
#[gpui::test]
async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
// Manipulate with multiple selections on a single line
cx.set_state(indoc! {"
dd«dd
»c«c
bb
aaaˇ»aa
"});
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
cx.assert_editor_state(indoc! {"
«aaaaa
bb
ccc
ddddˇ»
"});
// Manipulate with multiple disjoin selections
cx.set_state(indoc! {"
5«
4
3
2
1ˇ»
dd«dd
ccc
bb
aaaˇ»aa
"});
cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
cx.assert_editor_state(indoc! {"
«1
2
3
4
5ˇ»
«aaaaa
bb
ccc
ddddˇ»
"});
}
#[gpui::test]
fn test_duplicate_line(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@ -61,6 +61,7 @@ enum FoldMarkers {}
struct SelectionLayout {
head: DisplayPoint,
cursor_shape: CursorShape,
is_newest: bool,
range: Range<DisplayPoint>,
}
@ -70,6 +71,7 @@ impl SelectionLayout {
line_mode: bool,
cursor_shape: CursorShape,
map: &DisplaySnapshot,
is_newest: bool,
) -> Self {
if line_mode {
let selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
@ -77,6 +79,7 @@ impl SelectionLayout {
Self {
head: selection.head().to_display_point(map),
cursor_shape,
is_newest,
range: point_range.start.to_display_point(map)
..point_range.end.to_display_point(map),
}
@ -85,6 +88,7 @@ impl SelectionLayout {
Self {
head: selection.head(),
cursor_shape,
is_newest,
range: selection.range(),
}
}
@ -537,6 +541,24 @@ impl EditorElement {
corner_radius: 0.,
});
}
for (wrap_position, active) in layout.wrap_guides.iter() {
let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.;
let color = if *active {
self.style.active_wrap_guide
} else {
self.style.wrap_guide
};
scene.push_quad(Quad {
bounds: RectF::new(
vec2f(x, text_bounds.origin_y()),
vec2f(1., text_bounds.height()),
),
background: Some(color),
border: Border::new(0., Color::transparent_black()),
corner_radius: 0.,
});
}
}
}
@ -864,6 +886,12 @@ impl EditorElement {
let x = cursor_character_x - scroll_left;
let y = cursor_position.row() as f32 * layout.position_map.line_height
- scroll_top;
if selection.is_newest {
editor.pixel_position_of_newest_cursor = Some(vec2f(
bounds.origin_x() + x + block_width / 2.,
bounds.origin_y() + y + layout.position_map.line_height / 2.,
));
}
cursors.push(Cursor {
color: selection_style.cursor,
block_width,
@ -1310,16 +1338,15 @@ impl EditorElement {
}
}
fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 {
let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1;
fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> f32 {
let style = &self.style;
cx.text_layout_cache()
.layout_str(
"1".repeat(digit_count).as_str(),
" ".repeat(column).as_str(),
style.text.font_size,
&[(
digit_count,
column,
RunStyle {
font_id: style.text.font_id,
color: Color::black(),
@ -1330,6 +1357,11 @@ impl EditorElement {
.width()
}
fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 {
let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1;
self.column_pixels(digit_count, cx)
}
//Folds contained in a hunk are ignored apart from shrinking visual size
//If a fold contains any hunks then that fold line is marked as modified
fn layout_git_gutters(
@ -1977,6 +2009,7 @@ impl Element<Editor> for EditorElement {
let snapshot = editor.snapshot(cx);
let style = self.style.clone();
let line_height = (style.text.font_size * style.line_height_scalar).round();
let gutter_padding;
@ -2014,6 +2047,12 @@ impl Element<Editor> for EditorElement {
}
};
let wrap_guides = editor
.wrap_guides(cx)
.iter()
.map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
.collect();
let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height;
if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
size.set_y(
@ -2108,6 +2147,7 @@ impl Element<Editor> for EditorElement {
line_mode,
cursor_shape,
&snapshot.display_snapshot,
false,
));
}
selections.extend(remote_selections);
@ -2117,6 +2157,7 @@ impl Element<Editor> for EditorElement {
.selections
.disjoint_in_range(start_anchor..end_anchor, cx);
local_selections.extend(editor.selections.pending(cx));
let newest = editor.selections.newest(cx);
for selection in &local_selections {
let is_empty = selection.start == selection.end;
let selection_start = snapshot.prev_line_boundary(selection.start).1;
@ -2139,11 +2180,13 @@ impl Element<Editor> for EditorElement {
local_selections
.into_iter()
.map(|selection| {
let is_newest = selection == newest;
SelectionLayout::new(
selection,
editor.selections.line_mode,
editor.cursor_shape,
&snapshot.display_snapshot,
is_newest,
)
})
.collect(),
@ -2370,6 +2413,7 @@ impl Element<Editor> for EditorElement {
snapshot,
}),
visible_display_row_range: start_row..end_row,
wrap_guides,
gutter_size,
gutter_padding,
text_size,
@ -2520,6 +2564,7 @@ pub struct LayoutState {
gutter_margin: f32,
text_size: Vector2F,
mode: EditorMode,
wrap_guides: SmallVec<[(f32, bool); 2]>,
visible_display_row_range: Range<u32>,
active_rows: BTreeMap<u32, bool>,
highlighted_rows: Option<Range<u32>>,

View File

@ -198,7 +198,7 @@ fn show_hover(
// Construct new hover popover from hover request
let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| {
if hover_result.contents.is_empty() {
if hover_result.is_empty() {
return None;
}
@ -420,7 +420,7 @@ fn render_blocks(
RenderedInfo {
theme_id,
text,
text: text.trim().to_string(),
highlights,
region_ranges,
regions,
@ -816,6 +816,118 @@ mod tests {
});
}
#[gpui::test]
async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
// Hover with keyboard has no delay
cx.set_state(indoc! {"
fˇn test() { println!(); }
"});
cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
let symbol_range = cx.lsp_range(indoc! {"
«fn» test() { println!(); }
"});
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Array(vec![
lsp::MarkedString::String("regular text for hover to show".to_string()),
lsp::MarkedString::String("".to_string()),
lsp::MarkedString::LanguageString(lsp::LanguageString {
language: "Rust".to_string(),
value: "".to_string(),
}),
]),
range: Some(symbol_range),
}))
})
.next()
.await;
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
assert_eq!(
editor.hover_state.info_popover.clone().unwrap().blocks,
vec![HoverBlock {
text: "regular text for hover to show".to_string(),
kind: HoverBlockKind::Markdown,
}],
"No empty string hovers should be shown"
);
});
}
#[gpui::test]
async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
// Hover with keyboard has no delay
cx.set_state(indoc! {"
fˇn test() { println!(); }
"});
cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
let symbol_range = cx.lsp_range(indoc! {"
«fn» test() { println!(); }
"});
let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
let markdown_string = format!("\n```rust\n{code_str}```");
let closure_markdown_string = markdown_string.clone();
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
let future_markdown_string = closure_markdown_string.clone();
async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: future_markdown_string,
}),
range: Some(symbol_range),
}))
}
})
.next()
.await;
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, cx| {
let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
assert_eq!(
blocks,
vec![HoverBlock {
text: markdown_string,
kind: HoverBlockKind::Markdown,
}],
);
let style = editor.style(cx);
let rendered = render_blocks(0, &blocks, &Default::default(), None, &style);
assert_eq!(
rendered.text,
code_str.trim(),
"Should not have extra line breaks at end of rendered hover"
);
});
}
#[gpui::test]
async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});

View File

@ -195,20 +195,41 @@ impl InlayHintCache {
}
}
pub fn refresh_inlay_hints(
pub fn spawn_hint_refresh(
&mut self,
mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy,
cx: &mut ViewContext<Editor>,
) {
if !self.enabled || excerpts_to_query.is_empty() {
return;
) -> Option<InlaySplice> {
if !self.enabled {
return None;
}
let update_tasks = &mut self.update_tasks;
let mut invalidated_hints = Vec::new();
if invalidate.should_invalidate() {
update_tasks
.retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
let mut changed = false;
update_tasks.retain(|task_excerpt_id, _| {
let retain = excerpts_to_query.contains_key(task_excerpt_id);
changed |= !retain;
retain
});
self.hints.retain(|cached_excerpt, cached_hints| {
let retain = excerpts_to_query.contains_key(cached_excerpt);
changed |= !retain;
if !retain {
invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id));
}
retain
});
if changed {
self.version += 1;
}
}
if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
return None;
}
let cache_version = self.version;
excerpts_to_query.retain(|visible_excerpt_id, _| {
match update_tasks.entry(*visible_excerpt_id) {
@ -229,6 +250,15 @@ impl InlayHintCache {
.ok();
})
.detach();
if invalidated_hints.is_empty() {
None
} else {
Some(InlaySplice {
to_remove: invalidated_hints,
to_insert: Vec::new(),
})
}
}
fn new_allowed_hint_kinds_splice(
@ -684,7 +714,7 @@ async fn fetch_and_update_hints(
if query.invalidate.should_invalidate() {
let mut outdated_excerpt_caches = HashSet::default();
for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() {
for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
let excerpt_hints = excerpt_hints.read();
if excerpt_hints.buffer_id == query.buffer_id
&& excerpt_id != &query.excerpt_id
@ -1022,9 +1052,9 @@ mod tests {
"Should get its first hints when opening the editor"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
editor.inlay_hint_cache().version,
edits_made,
"The editor update the cache version after every cache/view change"
);
});
@ -1053,9 +1083,9 @@ mod tests {
"Should not update hints while the work task is running"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
editor.inlay_hint_cache().version,
edits_made,
"Should not update the cache while the work task is running"
);
});
@ -1077,9 +1107,9 @@ mod tests {
"New hints should be queried after the work task is done"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
editor.inlay_hint_cache().version,
edits_made,
"Cache version should udpate once after the work task is done"
);
});
@ -1194,9 +1224,9 @@ mod tests {
"Should get its first hints when opening the editor"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, 1,
editor.inlay_hint_cache().version,
1,
"Rust editor update the cache version after every cache/view change"
);
});
@ -1252,8 +1282,7 @@ mod tests {
"Markdown editor should have a separate verison, repeating Rust editor rules"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 1);
assert_eq!(editor.inlay_hint_cache().version, 1);
});
rs_editor.update(cx, |editor, cx| {
@ -1269,9 +1298,9 @@ mod tests {
"Rust inlay cache should change after the edit"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, 2,
editor.inlay_hint_cache().version,
2,
"Every time hint cache changes, cache version should be incremented"
);
});
@ -1283,8 +1312,7 @@ mod tests {
"Markdown editor should not be affected by Rust editor changes"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 1);
assert_eq!(editor.inlay_hint_cache().version, 1);
});
md_editor.update(cx, |editor, cx| {
@ -1300,8 +1328,7 @@ mod tests {
"Rust editor should not be affected by Markdown editor changes"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 2);
assert_eq!(editor.inlay_hint_cache().version, 2);
});
rs_editor.update(cx, |editor, cx| {
let expected_layers = vec!["1".to_string()];
@ -1311,8 +1338,7 @@ mod tests {
"Markdown editor should also change independently"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 2);
assert_eq!(editor.inlay_hint_cache().version, 2);
});
}
@ -1433,9 +1459,9 @@ mod tests {
vec!["other hint".to_string(), "type hint".to_string()],
visible_hint_labels(editor, cx)
);
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
editor.inlay_hint_cache().version,
edits_made,
"Should not update cache version due to new loaded hints being the same"
);
});
@ -1568,9 +1594,8 @@ mod tests {
);
assert!(cached_hint_labels(editor).is_empty());
assert!(visible_hint_labels(editor, cx).is_empty());
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, edits_made,
editor.inlay_hint_cache().version, edits_made,
"The editor should not update the cache version after /refresh query without updates"
);
});
@ -1641,8 +1666,7 @@ mod tests {
vec!["parameter hint".to_string()],
visible_hint_labels(editor, cx),
);
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, edits_made);
assert_eq!(editor.inlay_hint_cache().version, edits_made);
});
}
@ -1720,9 +1744,8 @@ mod tests {
"Should get hints from the last edit landed only"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, 1,
editor.inlay_hint_cache().version, 1,
"Only one update should be registered in the cache after all cancellations"
);
});
@ -1766,9 +1789,9 @@ mod tests {
"Should get hints from the last edit landed only"
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, 2,
editor.inlay_hint_cache().version,
2,
"Should update the cache version once more, for the new change"
);
});
@ -1886,9 +1909,8 @@ mod tests {
"Should have hints from both LSP requests made for a big file"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(
inlay_cache.version, 2,
editor.inlay_hint_cache().version, 2,
"Both LSP queries should've bumped the cache version"
);
});
@ -1918,8 +1940,7 @@ mod tests {
assert_eq!(expected_layers, cached_hint_labels(editor),
"Should have hints from the new LSP response after edit");
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added");
});
}
@ -2075,6 +2096,7 @@ mod tests {
panic!("unexpected uri: {:?}", params.text_document.uri);
};
// one hint per excerpt
let positions = [
lsp::Position::new(0, 2),
lsp::Position::new(4, 2),
@ -2138,8 +2160,7 @@ mod tests {
"When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison");
});
editor.update(cx, |editor, cx| {
@ -2169,8 +2190,8 @@ mod tests {
assert_eq!(expected_layers, cached_hint_labels(editor),
"With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 9);
assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(),
"Due to every excerpt having one hint, we update cache per new excerpt scrolled");
});
editor.update(cx, |editor, cx| {
@ -2179,7 +2200,7 @@ mod tests {
});
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let last_scroll_update_version = editor.update(cx, |editor, cx| {
let expected_layers = vec![
"main hint #0".to_string(),
"main hint #1".to_string(),
@ -2197,8 +2218,8 @@ mod tests {
assert_eq!(expected_layers, cached_hint_labels(editor),
"After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 12);
assert_eq!(editor.inlay_hint_cache().version, expected_layers.len());
expected_layers.len()
});
editor.update(cx, |editor, cx| {
@ -2225,12 +2246,14 @@ mod tests {
assert_eq!(expected_layers, cached_hint_labels(editor),
"After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
});
editor_edited.store(true, Ordering::Release);
editor.update(cx, |editor, cx| {
editor.change_selections(None, cx, |s| {
s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
});
editor.handle_input("++++more text++++", cx);
});
cx.foreground().run_until_parked();
@ -2240,19 +2263,253 @@ mod tests {
"main hint(edited) #1".to_string(),
"main hint(edited) #2".to_string(),
"main hint(edited) #3".to_string(),
"other hint #0".to_string(),
"other hint #1".to_string(),
"other hint #2".to_string(),
"other hint #3".to_string(),
"other hint #4".to_string(),
"other hint #5".to_string(),
"main hint(edited) #4".to_string(),
"main hint(edited) #5".to_string(),
"other hint(edited) #0".to_string(),
"other hint(edited) #1".to_string(),
];
assert_eq!(expected_layers, cached_hint_labels(editor),
"After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \
unedited (2nd) buffer should have the same hint");
assert_eq!(
expected_layers,
cached_hint_labels(editor),
"After multibuffer edit, editor gets scolled back to the last selection; \
all hints should be invalidated and requeried for all of its visible excerpts"
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
assert_eq!(inlay_cache.version, 16);
assert_eq!(
editor.inlay_hint_cache().version,
last_scroll_update_version + expected_layers.len() + 1,
"Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal"
);
});
}
#[gpui::test]
async fn test_excerpts_removed(
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: false,
show_parameter_hints: false,
show_other_hints: false,
})
});
let mut language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities: lsp::ServerCapabilities {
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
..Default::default()
},
..Default::default()
}))
.await;
let language = Arc::new(language);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/a",
json!({
"main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
"other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
}),
)
.await;
let project = Project::test(fs, ["/a".as_ref()], cx).await;
project.update(cx, |project, _| {
project.languages().add(Arc::clone(&language))
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let worktree_id = workspace.update(cx, |workspace, cx| {
workspace.project().read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
})
});
let buffer_1 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "main.rs"), cx)
})
.await
.unwrap();
let buffer_2 = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, "other.rs"), cx)
})
.await
.unwrap();
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
let buffer_1_excerpts = multibuffer.push_excerpts(
buffer_1.clone(),
[ExcerptRange {
context: Point::new(0, 0)..Point::new(2, 0),
primary: None,
}],
cx,
);
let buffer_2_excerpts = multibuffer.push_excerpts(
buffer_2.clone(),
[ExcerptRange {
context: Point::new(0, 1)..Point::new(2, 1),
primary: None,
}],
cx,
);
(buffer_1_excerpts, buffer_2_excerpts)
});
assert!(!buffer_1_excerpts.is_empty());
assert!(!buffer_2_excerpts.is_empty());
deterministic.run_until_parked();
cx.foreground().run_until_parked();
let (_, editor) =
cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx));
let editor_edited = Arc::new(AtomicBool::new(false));
let fake_server = fake_servers.next().await.unwrap();
let closure_editor_edited = Arc::clone(&editor_edited);
fake_server
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
let task_editor_edited = Arc::clone(&closure_editor_edited);
async move {
let hint_text = if params.text_document.uri
== lsp::Url::from_file_path("/a/main.rs").unwrap()
{
"main hint"
} else if params.text_document.uri
== lsp::Url::from_file_path("/a/other.rs").unwrap()
{
"other hint"
} else {
panic!("unexpected uri: {:?}", params.text_document.uri);
};
let positions = [
lsp::Position::new(0, 2),
lsp::Position::new(4, 2),
lsp::Position::new(22, 2),
lsp::Position::new(44, 2),
lsp::Position::new(56, 2),
lsp::Position::new(67, 2),
];
let out_of_range_hint = lsp::InlayHint {
position: lsp::Position::new(
params.range.start.line + 99,
params.range.start.character + 99,
),
label: lsp::InlayHintLabel::String(
"out of excerpt range, should be ignored".to_string(),
),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
};
let edited = task_editor_edited.load(Ordering::Acquire);
Ok(Some(
std::iter::once(out_of_range_hint)
.chain(positions.into_iter().enumerate().map(|(i, position)| {
lsp::InlayHint {
position,
label: lsp::InlayHintLabel::String(format!(
"{hint_text}{} #{i}",
if edited { "(edited)" } else { "" },
)),
kind: None,
text_edits: None,
tooltip: None,
padding_left: None,
padding_right: None,
data: None,
}
}))
.collect(),
))
}
})
.next()
.await;
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
assert_eq!(
vec!["main hint #0".to_string(), "other hint #0".to_string()],
cached_hint_labels(editor),
"Cache should update for both excerpts despite hints display was disabled"
);
assert!(
visible_hint_labels(editor, cx).is_empty(),
"All hints are disabled and should not be shown despite being present in the cache"
);
assert_eq!(
editor.inlay_hint_cache().version,
2,
"Cache should update once per excerpt query"
);
});
editor.update(cx, |editor, cx| {
editor.buffer().update(cx, |multibuffer, cx| {
multibuffer.remove_excerpts(buffer_2_excerpts, cx)
})
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
assert_eq!(
vec!["main hint #0".to_string()],
cached_hint_labels(editor),
"For the removed excerpt, should clean corresponding cached hints"
);
assert!(
visible_hint_labels(editor, cx).is_empty(),
"All hints are disabled and should not be shown despite being present in the cache"
);
assert_eq!(
editor.inlay_hint_cache().version,
3,
"Excerpt removal should trigger cache update"
);
});
update_test_language_settings(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: true,
show_parameter_hints: true,
show_other_hints: true,
})
});
cx.foreground().run_until_parked();
editor.update(cx, |editor, cx| {
let expected_hints = vec!["main hint #0".to_string()];
assert_eq!(
expected_hints,
cached_hint_labels(editor),
"Hint display settings change should not change the cache"
);
assert_eq!(
expected_hints,
visible_hint_labels(editor, cx),
"Settings change should make cached hints visible"
);
assert_eq!(
editor.inlay_hint_cache().version,
4,
"Settings change should trigger cache update"
);
});
}

View File

@ -7,8 +7,10 @@ use anyhow::{Context, Result};
use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle,
Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
elements::*,
geometry::vector::{vec2f, Vector2F},
AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
@ -750,6 +752,10 @@ impl Item for Editor {
Some(Box::new(handle.clone()))
}
fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
self.pixel_position_of_newest_cursor
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft { flex: None }
}
@ -946,21 +952,27 @@ impl SearchableItem for Editor {
cx: &mut ViewContext<Self>,
) {
self.unfold_ranges([matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index]);
self.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([matches[index].clone()])
});
s.select_ranges([range]);
})
}
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
self.unfold_ranges(matches.clone(), false, false, cx);
self.change_selections(None, cx, |s| s.select_ranges(matches));
let mut ranges = Vec::new();
for m in &matches {
ranges.push(self.range_for_match(&m))
}
self.change_selections(None, cx, |s| s.select_ranges(ranges));
}
fn match_index_for_direction(
&mut self,
matches: &Vec<Range<Anchor>>,
mut current_index: usize,
current_index: usize,
direction: Direction,
count: usize,
cx: &mut ViewContext<Self>,
) -> usize {
let buffer = self.buffer().read(cx).snapshot(cx);
@ -969,40 +981,39 @@ impl SearchableItem for Editor {
} else {
matches[current_index].start
};
if matches[current_index]
.start
.cmp(&current_index_position, &buffer)
.is_gt()
{
if direction == Direction::Prev {
if current_index == 0 {
current_index = matches.len() - 1;
let mut count = count % matches.len();
if count == 0 {
return current_index;
}
match direction {
Direction::Next => {
if matches[current_index]
.start
.cmp(&current_index_position, &buffer)
.is_gt()
{
count = count - 1
}
(current_index + count) % matches.len()
}
Direction::Prev => {
if matches[current_index]
.end
.cmp(&current_index_position, &buffer)
.is_lt()
{
count = count - 1;
}
if current_index >= count {
current_index - count
} else {
current_index -= 1;
matches.len() - (count - current_index)
}
}
} else if matches[current_index]
.end
.cmp(&current_index_position, &buffer)
.is_lt()
{
if direction == Direction::Next {
current_index = 0;
}
} else if direction == Direction::Prev {
if current_index == 0 {
current_index = matches.len() - 1;
} else {
current_index -= 1;
}
} else if direction == Direction::Next {
if current_index == matches.len() - 1 {
current_index = 0
} else {
current_index += 1;
}
};
current_index
}
}
fn find_matches(

View File

@ -138,7 +138,7 @@ impl SelectionsCollection {
.collect()
}
// Returns all of the selections, adjusted to take into account the selection line_mode
/// Returns all of the selections, adjusted to take into account the selection line_mode
pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec<Selection<Point>> {
let mut selections = self.all::<Point>(cx);
if self.line_mode {

View File

@ -1,6 +1,6 @@
use anyhow::Result;
use collections::HashMap;
use git2::{BranchType, ErrorCode};
use git2::{BranchType, StatusShow};
use parking_lot::Mutex;
use rpc::proto;
use serde_derive::{Deserialize, Serialize};
@ -10,6 +10,7 @@ use std::{
os::unix::prelude::OsStrExt,
path::{Component, Path, PathBuf},
sync::Arc,
time::SystemTime,
};
use sum_tree::{MapSeekTarget, TreeMap};
use util::ResultExt;
@ -25,24 +26,30 @@ pub struct Branch {
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
fn branch_name(&self) -> Option<String>;
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
/// Get the statuses of all of the files in the index that start with the given
/// path and have changes with resepect to the HEAD commit. This is fast because
/// the index stores hashes of trees, so that unchanged directories can be skipped.
fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus>;
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
/// Get the status of a given file in the working directory with respect to
/// the index. In the common case, when there are no changes, this only requires
/// an index lookup. The index stores the mtime of each file when it was added,
/// so there's no work to do if the mtime matches.
fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
fn branches(&self) -> Result<Vec<Branch>> {
Ok(vec![])
}
fn change_branch(&self, _: &str) -> Result<()> {
Ok(())
}
fn create_branch(&self, _: &str) -> Result<()> {
Ok(())
}
/// Get the status of a given file in the working directory with respect to
/// the HEAD commit. In the common case, when there are no changes, this only
/// requires an index lookup and blob comparison between the index and the HEAD
/// commit. The index stores the mtime of each file when it was added, so there's
/// no need to consider the working directory file if the mtime matches.
fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus>;
fn branches(&self) -> Result<Vec<Branch>>;
fn change_branch(&self, _: &str) -> Result<()>;
fn create_branch(&self, _: &str) -> Result<()>;
}
impl std::fmt::Debug for dyn GitRepository {
@ -51,7 +58,6 @@ impl std::fmt::Debug for dyn GitRepository {
}
}
#[async_trait::async_trait]
impl GitRepository for LibGitRepository {
fn reload_index(&self) {
if let Ok(mut index) = self.index() {
@ -89,39 +95,67 @@ impl GitRepository for LibGitRepository {
Some(branch.to_string())
}
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
let statuses = self.statuses(None).log_err()?;
fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
let mut map = TreeMap::default();
for status in statuses
.iter()
.filter(|status| !status.status().contains(git2::Status::IGNORED))
{
let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
let Some(status) = read_status(status.status()) else {
continue
};
let mut options = git2::StatusOptions::new();
options.pathspec(path_prefix);
options.show(StatusShow::Index);
map.insert(path, status)
}
Some(map)
}
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
let status = self.status_file(path);
match status {
Ok(status) => Ok(read_status(status)),
Err(e) => {
if e.code() == ErrorCode::NotFound {
Ok(None)
} else {
Err(e.into())
if let Some(statuses) = self.statuses(Some(&mut options)).log_err() {
for status in statuses.iter() {
let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes())));
let status = status.status();
if !status.contains(git2::Status::IGNORED) {
if let Some(status) = read_status(status) {
map.insert(path, status)
}
}
}
}
map
}
fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
// If the file has not changed since it was added to the index, then
// there can't be any changes.
if matches_index(self, path, mtime) {
return None;
}
let mut options = git2::StatusOptions::new();
options.pathspec(&path.0);
options.disable_pathspec_match(true);
options.include_untracked(true);
options.recurse_untracked_dirs(true);
options.include_unmodified(true);
options.show(StatusShow::Workdir);
let statuses = self.statuses(Some(&mut options)).log_err()?;
let status = statuses.get(0).and_then(|s| read_status(s.status()));
status
}
fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option<GitFileStatus> {
let mut options = git2::StatusOptions::new();
options.pathspec(&path.0);
options.disable_pathspec_match(true);
options.include_untracked(true);
options.recurse_untracked_dirs(true);
options.include_unmodified(true);
// If the file has not changed since it was added to the index, then
// there's no need to examine the working directory file: just compare
// the blob in the index to the one in the HEAD commit.
if matches_index(self, path, mtime) {
options.show(StatusShow::Index);
}
let statuses = self.statuses(Some(&mut options)).log_err()?;
let status = statuses.get(0).and_then(|s| read_status(s.status()));
status
}
fn branches(&self) -> Result<Vec<Branch>> {
let local_branches = self.branches(Some(BranchType::Local))?;
let valid_branches = local_branches
@ -164,6 +198,21 @@ impl GitRepository for LibGitRepository {
}
}
fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool {
if let Some(index) = repo.index().log_err() {
if let Some(entry) = index.get_path(&path, 0) {
if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() {
if entry.mtime.seconds() == mtime.as_secs() as i32
&& entry.mtime.nanoseconds() == mtime.subsec_nanos()
{
return true;
}
}
}
}
false
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
if status.contains(git2::Status::CONFLICTED) {
Some(GitFileStatus::Conflict)
@ -213,18 +262,40 @@ impl GitRepository for FakeGitRepository {
state.branch_name.clone()
}
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>> {
let state = self.state.lock();
fn staged_statuses(&self, path_prefix: &Path) -> TreeMap<RepoPath, GitFileStatus> {
let mut map = TreeMap::default();
let state = self.state.lock();
for (repo_path, status) in state.worktree_statuses.iter() {
map.insert(repo_path.to_owned(), status.to_owned());
if repo_path.0.starts_with(path_prefix) {
map.insert(repo_path.to_owned(), status.to_owned());
}
}
Some(map)
map
}
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>> {
fn unstaged_status(&self, _path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
None
}
fn status(&self, path: &RepoPath, _mtime: SystemTime) -> Option<GitFileStatus> {
let state = self.state.lock();
Ok(state.worktree_statuses.get(path).cloned())
state.worktree_statuses.get(path).cloned()
}
fn branches(&self) -> Result<Vec<Branch>> {
Ok(vec![])
}
fn change_branch(&self, name: &str) -> Result<()> {
let mut state = self.state.lock();
state.branch_name = Some(name.to_owned());
Ok(())
}
fn create_branch(&self, name: &str) -> Result<()> {
let mut state = self.state.lock();
state.branch_name = Some(name.to_owned());
Ok(())
}
}

View File

@ -3411,18 +3411,14 @@ impl<'a, 'b, 'c, V: View> 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
.bindings_for_action(action.id())
.find_map(|b| {
let highest_handler = handler_depth?;
if action.eq(b.action())
&& (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..]))
{
Some(b.keystrokes().into())
} else {
None
}
})
.keystrokes_for_action(action, action_contexts)
}
fn notify_if_view_ancestors_change(&mut self, view_id: usize) {

View File

@ -10,8 +10,8 @@ use crate::{
mac::{
platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer, screen::Screen,
},
Event, InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent,
MouseMovedEvent, Scene, WindowBounds, WindowKind,
Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton,
MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind,
},
};
use block::ConcreteBlock;
@ -1053,7 +1053,44 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
let window_height = window_state_borrow.content_size().y();
let event = unsafe { Event::from_native(native_event, Some(window_height)) };
if let Some(event) = event {
if let Some(mut event) = event {
let synthesized_second_event = match &mut event {
Event::MouseDown(
event @ MouseButtonEvent {
button: MouseButton::Left,
modifiers: Modifiers { ctrl: true, .. },
..
},
) => {
*event = MouseButtonEvent {
button: MouseButton::Right,
modifiers: Modifiers {
ctrl: false,
..event.modifiers
},
click_count: 1,
..*event
};
Some(Event::MouseUp(MouseButtonEvent {
button: MouseButton::Right,
..*event
}))
}
// Because we map a ctrl-left_down to a right_down -> right_up let's ignore
// the ctrl-left_up to avoid having a mismatch in button down/up events if the
// user is still holding ctrl when releasing the left mouse button
Event::MouseUp(MouseButtonEvent {
button: MouseButton::Left,
modifiers: Modifiers { ctrl: true, .. },
..
}) => return,
_ => None,
};
match &event {
Event::MouseMoved(
event @ MouseMovedEvent {
@ -1105,6 +1142,9 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
if let Some(mut callback) = window_state_borrow.event_callback.take() {
drop(window_state_borrow);
callback(event);
if let Some(event) = synthesized_second_event {
callback(event);
}
window_state.borrow_mut().event_callback = Some(callback);
}
}

View File

@ -44,6 +44,8 @@ pub struct LanguageSettings {
pub hard_tabs: bool,
pub soft_wrap: SoftWrap,
pub preferred_line_length: u32,
pub show_wrap_guides: bool,
pub wrap_guides: Vec<usize>,
pub format_on_save: FormatOnSave,
pub remove_trailing_whitespace_on_save: bool,
pub ensure_final_newline_on_save: bool,
@ -84,6 +86,10 @@ pub struct LanguageSettingsContent {
#[serde(default)]
pub preferred_line_length: Option<u32>,
#[serde(default)]
pub show_wrap_guides: Option<bool>,
#[serde(default)]
pub wrap_guides: Option<Vec<usize>>,
#[serde(default)]
pub format_on_save: Option<FormatOnSave>,
#[serde(default)]
pub remove_trailing_whitespace_on_save: Option<bool>,
@ -378,6 +384,9 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
merge(&mut settings.tab_size, src.tab_size);
merge(&mut settings.hard_tabs, src.hard_tabs);
merge(&mut settings.soft_wrap, src.soft_wrap);
merge(&mut settings.show_wrap_guides, src.show_wrap_guides);
merge(&mut settings.wrap_guides, src.wrap_guides.clone());
merge(
&mut settings.preferred_line_length,
src.preferred_line_length,

View File

@ -6,7 +6,7 @@ import ScreenCaptureKit
class LKRoomDelegate: RoomDelegate {
var data: UnsafeRawPointer
var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void
var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void
var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void
var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void
var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void
var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void
@ -16,7 +16,7 @@ class LKRoomDelegate: RoomDelegate {
init(
data: UnsafeRawPointer,
onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void,
@ -43,7 +43,7 @@ class LKRoomDelegate: RoomDelegate {
if track.kind == .video {
self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
} else if track.kind == .audio {
self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque())
self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque(), Unmanaged.passUnretained(publication).toOpaque())
}
}
@ -52,12 +52,12 @@ class LKRoomDelegate: RoomDelegate {
self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted)
}
}
func room(_ room: Room, didUpdate speakers: [Participant]) {
guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return }
self.onActiveSpeakersChanged(self.data, speaker_ids)
}
func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) {
if track.kind == .video {
self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString)
@ -104,7 +104,7 @@ class LKVideoRenderer: NSObject, VideoRenderer {
public func LKRoomDelegateCreate(
data: UnsafeRawPointer,
onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void,
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void,
onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void,
onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void,
onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void,
onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void,
@ -180,39 +180,39 @@ public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawP
@_cdecl("LKRoomAudioTracksForRemoteParticipant")
public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
for (_, participant) in room.remoteParticipants {
if participant.identity == participantId as String {
return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray?
}
}
return nil;
}
@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant")
public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
for (_, participant) in room.remoteParticipants {
if participant.identity == participantId as String {
return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray?
}
}
return nil;
}
@_cdecl("LKRoomVideoTracksForRemoteParticipant")
public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? {
let room = Unmanaged<Room>.fromOpaque(room).takeUnretainedValue()
for (_, participant) in room.remoteParticipants {
if participant.identity == participantId as String {
return participant.videoTracks.compactMap { $0.track as? RemoteVideoTrack } as CFArray?
}
}
return nil;
}
@ -222,7 +222,7 @@ public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer {
echoCancellation: true,
noiseSuppression: true
))
return Unmanaged.passRetained(track).toOpaque()
}
@ -276,7 +276,7 @@ public func LKLocalTrackPublicationSetMute(
callback_data: UnsafeRawPointer
) {
let publication = Unmanaged<LocalTrackPublication>.fromOpaque(publication).takeUnretainedValue()
if muted {
publication.mute().then {
on_complete(callback_data, nil)
@ -307,3 +307,21 @@ public func LKRemoteTrackPublicationSetEnabled(
on_complete(callback_data, error.localizedDescription as CFString)
}
}
@_cdecl("LKRemoteTrackPublicationIsMuted")
public func LKRemoteTrackPublicationIsMuted(
publication: UnsafeRawPointer
) -> Bool {
let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
return publication.muted
}
@_cdecl("LKRemoteTrackPublicationGetSid")
public func LKRemoteTrackPublicationGetSid(
publication: UnsafeRawPointer
) -> CFString {
let publication = Unmanaged<RemoteTrackPublication>.fromOpaque(publication).takeUnretainedValue()
return publication.sid as CFString
}

View File

@ -63,7 +63,7 @@ fn main() {
let audio_track = LocalAudioTrack::create();
let audio_track_publication = room_a.publish_audio_track(&audio_track).await.unwrap();
if let RemoteAudioTrackUpdate::Subscribed(track) =
if let RemoteAudioTrackUpdate::Subscribed(track, _) =
audio_track_updates.next().await.unwrap()
{
let remote_tracks = room_b.remote_audio_tracks("test-participant-1");

View File

@ -26,6 +26,7 @@ extern "C" {
publisher_id: CFStringRef,
track_id: CFStringRef,
remote_track: *const c_void,
remote_publication: *const c_void,
),
on_did_unsubscribe_from_remote_audio_track: extern "C" fn(
callback_data: *mut c_void,
@ -125,6 +126,9 @@ extern "C" {
on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef),
callback_data: *mut c_void,
);
fn LKRemoteTrackPublicationIsMuted(publication: *const c_void) -> bool;
fn LKRemoteTrackPublicationGetSid(publication: *const c_void) -> CFStringRef;
}
pub type Sid = String;
@ -372,11 +376,19 @@ impl Room {
rx
}
fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack) {
fn did_subscribe_to_remote_audio_track(
&self,
track: RemoteAudioTrack,
publication: RemoteTrackPublication,
) {
let track = Arc::new(track);
let publication = Arc::new(publication);
self.remote_audio_track_subscribers.lock().retain(|tx| {
tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone()))
.is_ok()
tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(
track.clone(),
publication.clone(),
))
.is_ok()
});
}
@ -501,13 +513,15 @@ impl RoomDelegate {
publisher_id: CFStringRef,
track_id: CFStringRef,
track: *const c_void,
publication: *const c_void,
) {
let room = unsafe { Weak::from_raw(room as *mut Room) };
let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() };
let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() };
let track = RemoteAudioTrack::new(track, track_id, publisher_id);
let publication = RemoteTrackPublication::new(publication);
if let Some(room) = room.upgrade() {
room.did_subscribe_to_remote_audio_track(track);
room.did_subscribe_to_remote_audio_track(track, publication);
}
let _ = Weak::into_raw(room);
}
@ -682,6 +696,14 @@ impl RemoteTrackPublication {
Self(native_track_publication)
}
pub fn sid(&self) -> String {
unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() }
}
pub fn is_muted(&self) -> bool {
unsafe { LKRemoteTrackPublicationIsMuted(self.0) }
}
pub fn set_enabled(&self, enabled: bool) -> impl Future<Output = Result<()>> {
let (tx, rx) = futures::channel::oneshot::channel();
@ -832,7 +854,7 @@ pub enum RemoteVideoTrackUpdate {
pub enum RemoteAudioTrackUpdate {
ActiveSpeakersChanged { speakers: Vec<Sid> },
MuteChanged { track_id: Sid, muted: bool },
Subscribed(Arc<RemoteAudioTrack>),
Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}

View File

@ -216,6 +216,8 @@ impl TestServer {
publisher_id: identity.clone(),
});
let publication = Arc::new(RemoteTrackPublication);
room.audio_tracks.push(track.clone());
for (id, client_room) in &room.client_rooms {
@ -225,7 +227,10 @@ impl TestServer {
.lock()
.audio_track_updates
.0
.try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone()))
.try_broadcast(RemoteAudioTrackUpdate::Subscribed(
track.clone(),
publication.clone(),
))
.unwrap();
}
}
@ -501,6 +506,14 @@ impl RemoteTrackPublication {
pub fn set_enabled(&self, _enabled: bool) -> impl Future<Output = Result<()>> {
async { Ok(()) }
}
pub fn is_muted(&self) -> bool {
false
}
pub fn sid(&self) -> String {
"".to_string()
}
}
#[derive(Clone)]
@ -579,7 +592,7 @@ pub enum RemoteVideoTrackUpdate {
pub enum RemoteAudioTrackUpdate {
ActiveSpeakersChanged { speakers: Vec<Sid> },
MuteChanged { track_id: Sid, muted: bool },
Subscribed(Arc<RemoteAudioTrack>),
Subscribed(Arc<RemoteAudioTrack>, Arc<RemoteTrackPublication>),
Unsubscribed { publisher_id: Sid, track_id: Sid },
}

View File

@ -62,6 +62,14 @@ impl NodeRuntime {
args: &[&str],
) -> Result<Output> {
let attempt = |installation_path: PathBuf| async move {
let mut env_path = installation_path.join("bin").into_os_string();
if let Some(existing_path) = std::env::var_os("PATH") {
if !existing_path.is_empty() {
env_path.push(":");
env_path.push(&existing_path);
}
}
let node_binary = installation_path.join("bin/node");
let npm_file = installation_path.join("bin/npm");
@ -74,6 +82,7 @@ impl NodeRuntime {
}
let mut command = Command::new(node_binary);
command.env("PATH", env_path);
command.arg(npm_file).arg(subcommand).args(args);
if let Some(directory) = directory {

View File

@ -259,6 +259,7 @@ pub enum Event {
LanguageServerLog(LanguageServerId, String),
Notification(String),
ActiveEntryChanged(Option<ProjectEntryId>),
ActivateProjectPanel,
WorktreeAdded,
WorktreeRemoved(WorktreeId),
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
@ -425,6 +426,12 @@ pub struct Hover {
pub language: Option<Arc<Language>>,
}
impl Hover {
pub fn is_empty(&self) -> bool {
self.contents.iter().all(|block| block.text.is_empty())
}
}
#[derive(Default)]
pub struct ProjectTransaction(pub HashMap<ModelHandle<Buffer>, language::Transaction>);
@ -1909,7 +1916,9 @@ impl Project {
return;
}
let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap();
let abs_path = file.abs_path(cx);
let uri = lsp::Url::from_file_path(&abs_path)
.unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}"));
let initial_snapshot = buffer.text_snapshot();
let language = buffer.language().cloned();
let worktree_id = file.worktree_id(cx);

View File

@ -2015,37 +2015,6 @@ impl LocalSnapshot {
entry
}
#[must_use = "Changed paths must be used for diffing later"]
fn scan_statuses(
&mut self,
repo_ptr: &dyn GitRepository,
work_directory: &RepositoryWorkDirectory,
) -> Vec<Arc<Path>> {
let mut changes = vec![];
let mut edits = vec![];
let statuses = repo_ptr.statuses();
for mut entry in self
.descendent_entries(false, false, &work_directory.0)
.cloned()
{
let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else {
continue;
};
let repo_path = RepoPath(repo_path.to_path_buf());
let git_file_status = statuses.as_ref().and_then(|s| s.get(&repo_path).copied());
if entry.git_status != git_file_status {
entry.git_status = git_file_status;
changes.push(entry.path.clone());
edits.push(Edit::Insert(entry));
}
}
self.entries_by_path.edit(edits, &());
changes
}
fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet<u64> {
let mut inodes = TreeSet::default();
for ancestor in path.ancestors().skip(1) {
@ -2189,6 +2158,38 @@ impl BackgroundScannerState {
.any(|p| entry.path.starts_with(p))
}
fn enqueue_scan_dir(&self, abs_path: Arc<Path>, entry: &Entry, scan_job_tx: &Sender<ScanJob>) {
let path = entry.path.clone();
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
let mut containing_repository = None;
if !ignore_stack.is_all() {
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
containing_repository = Some((
workdir_path,
repo.repo_ptr.clone(),
repo.repo_ptr.lock().staged_statuses(repo_path),
));
}
}
}
if !ancestor_inodes.contains(&entry.inode) {
ancestor_inodes.insert(entry.inode);
scan_job_tx
.try_send(ScanJob {
abs_path,
path,
ignore_stack,
scan_queue: scan_job_tx.clone(),
ancestor_inodes,
is_external: entry.is_external,
containing_repository,
})
.unwrap();
}
}
fn reuse_entry_id(&mut self, entry: &mut Entry) {
if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) {
entry.id = removed_entry_id;
@ -2201,7 +2202,7 @@ impl BackgroundScannerState {
self.reuse_entry_id(&mut entry);
let entry = self.snapshot.insert_entry(entry, fs);
if entry.path.file_name() == Some(&DOT_GIT) {
self.build_repository(entry.path.clone(), fs);
self.build_git_repository(entry.path.clone(), fs);
}
#[cfg(test)]
@ -2215,7 +2216,6 @@ impl BackgroundScannerState {
parent_path: &Arc<Path>,
entries: impl IntoIterator<Item = Entry>,
ignore: Option<Arc<Gitignore>>,
fs: &dyn Fs,
) {
let mut parent_entry = if let Some(parent_entry) = self
.snapshot
@ -2244,16 +2244,12 @@ impl BackgroundScannerState {
.insert(abs_parent_path, (ignore, false));
}
self.scanned_dirs.insert(parent_entry.id);
let parent_entry_id = parent_entry.id;
self.scanned_dirs.insert(parent_entry_id);
let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
let mut entries_by_id_edits = Vec::new();
let mut dotgit_path = None;
for entry in entries {
if entry.path.file_name() == Some(&DOT_GIT) {
dotgit_path = Some(entry.path.clone());
}
entries_by_id_edits.push(Edit::Insert(PathEntry {
id: entry.id,
path: entry.path.clone(),
@ -2268,9 +2264,6 @@ impl BackgroundScannerState {
.edit(entries_by_path_edits, &());
self.snapshot.entries_by_id.edit(entries_by_id_edits, &());
if let Some(dotgit_path) = dotgit_path {
self.build_repository(dotgit_path, fs);
}
if let Err(ix) = self.changed_paths.binary_search(parent_path) {
self.changed_paths.insert(ix, parent_path.clone());
}
@ -2346,7 +2339,7 @@ impl BackgroundScannerState {
});
match repository {
None => {
self.build_repository(dot_git_dir.into(), fs);
self.build_git_repository(dot_git_dir.into(), fs);
}
Some((entry_id, repository)) => {
if repository.git_dir_scan_id == scan_id {
@ -2370,13 +2363,7 @@ impl BackgroundScannerState {
.repository_entries
.update(&work_dir, |entry| entry.branch = branch.map(Into::into));
let changed_paths = self.snapshot.scan_statuses(&*repository, &work_dir);
util::extend_sorted(
&mut self.changed_paths,
changed_paths,
usize::MAX,
Ord::cmp,
)
self.update_git_statuses(&work_dir, &*repository);
}
}
}
@ -2397,7 +2384,15 @@ impl BackgroundScannerState {
snapshot.repository_entries = repository_entries;
}
fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
fn build_git_repository(
&mut self,
dot_git_path: Arc<Path>,
fs: &dyn Fs,
) -> Option<(
RepositoryWorkDirectory,
Arc<Mutex<dyn GitRepository>>,
TreeMap<RepoPath, GitFileStatus>,
)> {
log::info!("build git repository {:?}", dot_git_path);
let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
@ -2429,22 +2424,54 @@ impl BackgroundScannerState {
},
);
let changed_paths = self
.snapshot
.scan_statuses(repo_lock.deref(), &work_directory);
let staged_statuses = self.update_git_statuses(&work_directory, &*repo_lock);
drop(repo_lock);
self.snapshot.git_repositories.insert(
work_dir_id,
LocalRepositoryEntry {
git_dir_scan_id: 0,
repo_ptr: repository,
repo_ptr: repository.clone(),
git_dir_path: dot_git_path.clone(),
},
);
util::extend_sorted(&mut self.changed_paths, changed_paths, usize::MAX, Ord::cmp);
Some(())
Some((work_directory, repository, staged_statuses))
}
fn update_git_statuses(
&mut self,
work_directory: &RepositoryWorkDirectory,
repo: &dyn GitRepository,
) -> TreeMap<RepoPath, GitFileStatus> {
let staged_statuses = repo.staged_statuses(Path::new(""));
let mut changes = vec![];
let mut edits = vec![];
for mut entry in self
.snapshot
.descendent_entries(false, false, &work_directory.0)
.cloned()
{
let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else {
continue;
};
let repo_path = RepoPath(repo_path.to_path_buf());
let git_file_status = combine_git_statuses(
staged_statuses.get(&repo_path).copied(),
repo.unstaged_status(&repo_path, entry.mtime),
);
if entry.git_status != git_file_status {
entry.git_status = git_file_status;
changes.push(entry.path.clone());
edits.push(Edit::Insert(entry));
}
}
self.snapshot.entries_by_path.edit(edits, &());
util::extend_sorted(&mut self.changed_paths, changes, usize::MAX, Ord::cmp);
staged_statuses
}
}
@ -3031,16 +3058,8 @@ impl BackgroundScanner {
) {
use futures::FutureExt as _;
let (root_abs_path, root_inode) = {
let snapshot = &self.state.lock().snapshot;
(
snapshot.abs_path.clone(),
snapshot.root_entry().map(|e| e.inode),
)
};
// Populate ignores above the root.
let ignore_stack;
let root_abs_path = self.state.lock().snapshot.abs_path.clone();
for ancestor in root_abs_path.ancestors().skip(1) {
if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await
{
@ -3051,31 +3070,24 @@ impl BackgroundScanner {
.insert(ancestor.into(), (ignore.into(), false));
}
}
let (scan_job_tx, scan_job_rx) = channel::unbounded();
{
let mut state = self.state.lock();
state.snapshot.scan_id += 1;
ignore_stack = state
.snapshot
.ignore_stack_for_abs_path(&root_abs_path, true);
if ignore_stack.is_all() {
if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
let ignore_stack = state
.snapshot
.ignore_stack_for_abs_path(&root_abs_path, true);
if ignore_stack.is_all() {
root_entry.is_ignored = true;
state.insert_entry(root_entry, self.fs.as_ref());
state.insert_entry(root_entry.clone(), self.fs.as_ref());
}
state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx);
}
};
// Perform an initial scan of the directory.
let (scan_job_tx, scan_job_rx) = channel::unbounded();
smol::block_on(scan_job_tx.send(ScanJob {
abs_path: root_abs_path,
path: Arc::from(Path::new("")),
ignore_stack,
ancestor_inodes: TreeSet::from_ordered_entries(root_inode),
is_external: false,
scan_queue: scan_job_tx.clone(),
}))
.unwrap();
drop(scan_job_tx);
self.scan_dirs(true, scan_job_rx).await;
{
@ -3263,20 +3275,7 @@ impl BackgroundScanner {
if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
if entry.kind == EntryKind::UnloadedDir {
let abs_path = root_path.join(ancestor);
let ignore_stack =
state.snapshot.ignore_stack_for_abs_path(&abs_path, true);
let ancestor_inodes =
state.snapshot.ancestor_inodes_for_path(&ancestor);
scan_job_tx
.try_send(ScanJob {
abs_path: abs_path.into(),
path: ancestor.into(),
ignore_stack,
scan_queue: scan_job_tx.clone(),
ancestor_inodes,
is_external: entry.is_external,
})
.unwrap();
state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx);
state.paths_to_scan.insert(path.clone());
break;
}
@ -3391,18 +3390,16 @@ impl BackgroundScanner {
let mut ignore_stack = job.ignore_stack.clone();
let mut new_ignore = None;
let (root_abs_path, root_char_bag, next_entry_id, repository) = {
let (root_abs_path, root_char_bag, next_entry_id) = {
let snapshot = &self.state.lock().snapshot;
(
snapshot.abs_path().clone(),
snapshot.root_char_bag,
self.next_entry_id.clone(),
snapshot
.local_repo_for_path(&job.path)
.map(|(work_dir, repo)| (work_dir, repo.clone())),
)
};
let mut dotgit_path = None;
let mut root_canonical_path = None;
let mut new_entries: Vec<Entry> = Vec::new();
let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
@ -3465,6 +3462,10 @@ impl BackgroundScanner {
}
}
}
// If we find a .git, we'll need to load the repository.
else if child_name == *DOT_GIT {
dotgit_path = Some(child_path.clone());
}
let mut child_entry = Entry::new(
child_path.clone(),
@ -3525,6 +3526,7 @@ impl BackgroundScanner {
},
ancestor_inodes,
scan_queue: job.scan_queue.clone(),
containing_repository: job.containing_repository.clone(),
}));
} else {
new_jobs.push(None);
@ -3532,14 +3534,17 @@ impl BackgroundScanner {
} else {
child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
if !child_entry.is_ignored {
if let Some((repo_path, repo)) = &repository {
if let Ok(path) = child_path.strip_prefix(&repo_path.0) {
child_entry.git_status = repo
.repo_ptr
.lock()
.status(&RepoPath(path.into()))
.log_err()
.flatten();
if let Some((repository_dir, repository, staged_statuses)) =
&job.containing_repository
{
if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) {
let repo_path = RepoPath(repo_path.into());
child_entry.git_status = combine_git_statuses(
staged_statuses.get(&repo_path).copied(),
repository
.lock()
.unstaged_status(&repo_path, child_entry.mtime),
);
}
}
}
@ -3549,27 +3554,39 @@ impl BackgroundScanner {
}
let mut state = self.state.lock();
let mut new_jobs = new_jobs.into_iter();
// Identify any subdirectories that should not be scanned.
let mut job_ix = 0;
for entry in &mut new_entries {
state.reuse_entry_id(entry);
if entry.is_dir() {
let new_job = new_jobs.next().expect("missing scan job for entry");
if state.should_scan_directory(&entry) {
if let Some(new_job) = new_job {
job.scan_queue
.try_send(new_job)
.expect("channel is unbounded");
}
job_ix += 1;
} else {
log::debug!("defer scanning directory {:?}", entry.path);
entry.kind = EntryKind::UnloadedDir;
new_jobs.remove(job_ix);
}
}
}
assert!(new_jobs.next().is_none());
state.populate_dir(&job.path, new_entries, new_ignore, self.fs.as_ref());
state.populate_dir(&job.path, new_entries, new_ignore);
let repository =
dotgit_path.and_then(|path| state.build_git_repository(path, self.fs.as_ref()));
for new_job in new_jobs {
if let Some(mut new_job) = new_job {
if let Some(containing_repository) = &repository {
new_job.containing_repository = Some(containing_repository.clone());
}
job.scan_queue
.try_send(new_job)
.expect("channel is unbounded");
}
}
Ok(())
}
@ -3638,13 +3655,10 @@ impl BackgroundScanner {
if let Some((work_dir, repo)) =
state.snapshot.local_repo_for_path(&path)
{
if let Ok(path) = path.strip_prefix(work_dir.0) {
fs_entry.git_status = repo
.repo_ptr
.lock()
.status(&RepoPath(path.into()))
.log_err()
.flatten()
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
let repo_path = RepoPath(repo_path.into());
let repo = repo.repo_ptr.lock();
fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime);
}
}
}
@ -3652,20 +3666,7 @@ impl BackgroundScanner {
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
if state.should_scan_directory(&fs_entry) {
let mut ancestor_inodes =
state.snapshot.ancestor_inodes_for_path(&path);
if !ancestor_inodes.contains(&metadata.inode) {
ancestor_inodes.insert(metadata.inode);
smol::block_on(scan_queue_tx.send(ScanJob {
abs_path,
path: path.clone(),
ignore_stack,
ancestor_inodes,
is_external: fs_entry.is_external,
scan_queue: scan_queue_tx.clone(),
}))
.unwrap();
}
state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx);
} else {
fs_entry.kind = EntryKind::UnloadedDir;
}
@ -3822,18 +3823,7 @@ impl BackgroundScanner {
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
let state = self.state.lock();
if state.should_scan_directory(&entry) {
job.scan_queue
.try_send(ScanJob {
abs_path: abs_path.clone(),
path: entry.path.clone(),
ignore_stack: child_ignore_stack.clone(),
scan_queue: job.scan_queue.clone(),
ancestor_inodes: state
.snapshot
.ancestor_inodes_for_path(&entry.path),
is_external: false,
})
.unwrap();
state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue);
}
}
@ -4022,6 +4012,11 @@ struct ScanJob {
scan_queue: Sender<ScanJob>,
ancestor_inodes: TreeSet<u64>,
is_external: bool,
containing_repository: Option<(
RepositoryWorkDirectory,
Arc<Mutex<dyn GitRepository>>,
TreeMap<RepoPath, GitFileStatus>,
)>,
}
struct UpdateIgnoreStatusJob {
@ -4348,3 +4343,22 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
}
}
}
fn combine_git_statuses(
staged: Option<GitFileStatus>,
unstaged: Option<GitFileStatus>,
) -> Option<GitFileStatus> {
if let Some(staged) = staged {
if let Some(unstaged) = unstaged {
if unstaged != staged {
Some(GitFileStatus::Modified)
} else {
Some(staged)
}
} else {
Some(staged)
}
} else {
unstaged
}
}

View File

@ -10,6 +10,7 @@ doctest = false
[dependencies]
context_menu = { path = "../context_menu" }
collections = { path = "../collections" }
db = { path = "../db" }
drag_and_drop = { path = "../drag_and_drop" }
editor = { path = "../editor" }

View File

@ -0,0 +1,103 @@
use std::{path::Path, str, sync::Arc};
use collections::HashMap;
use gpui::{AppContext, AssetSource};
use serde_derive::Deserialize;
use util::iife;
#[derive(Deserialize, Debug)]
struct TypeConfig {
icon: Arc<str>,
}
#[derive(Deserialize, Debug)]
pub struct FileAssociations {
suffixes: HashMap<String, String>,
types: HashMap<String, TypeConfig>,
}
const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder";
const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder";
const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron";
const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron";
pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json";
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
cx.set_global(FileAssociations::new(assets))
}
impl FileAssociations {
pub fn new(assets: impl AssetSource) -> Self {
assets
.load("icons/file_icons/file_types.json")
.and_then(|file| {
serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap())
.map_err(Into::into)
})
.unwrap_or_else(|_| FileAssociations {
suffixes: HashMap::default(),
types: HashMap::default(),
})
}
pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
iife!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
// FIXME: Associate a type with the languages and have the file's langauge
// override these associations
iife!({
let suffix = path
.file_name()
.and_then(|os_str| os_str.to_str())
.and_then(|file_name| {
file_name
.find('.')
.and_then(|dot_index| file_name.get(dot_index + 1..))
})?;
this.suffixes
.get(suffix)
.and_then(|type_str| this.types.get(type_str))
.map(|type_config| type_config.icon.clone())
})
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
})
.unwrap_or_else(|| Arc::from("".to_string()))
}
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
iife!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_DIRECTORY_TYPE
} else {
COLLAPSED_DIRECTORY_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
})
.unwrap_or_else(|| Arc::from("".to_string()))
}
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
iife!({
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
let key = if expanded {
EXPANDED_CHEVRON_TYPE
} else {
COLLAPSED_CHEVRON_TYPE
};
this.types
.get(key)
.map(|type_config| type_config.icon.clone())
})
.unwrap_or_else(|| Arc::from("".to_string()))
}
}

View File

@ -1,9 +1,12 @@
pub mod file_associations;
mod project_panel_settings;
use context_menu::{ContextMenu, ContextMenuItem};
use db::kvp::KEY_VALUE_STORE;
use drag_and_drop::{DragAndDrop, Draggable};
use editor::{Cancel, Editor};
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
use file_associations::FileAssociations;
use futures::stream::StreamExt;
use gpui::{
actions,
@ -15,8 +18,8 @@ use gpui::{
geometry::vector::Vector2F,
keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton, PromptLevel},
Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle,
Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
Action, AnyElement, AppContext, AssetSource, AsyncAppContext, ClipboardItem, Element, Entity,
ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{
@ -94,6 +97,7 @@ pub enum ClipboardEntry {
#[derive(Debug, PartialEq, Eq)]
pub struct EntryDetails {
filename: String,
icon: Option<Arc<str>>,
path: Arc<Path>,
depth: usize,
kind: EntryKind,
@ -121,7 +125,9 @@ actions!(
Paste,
Delete,
Rename,
ToggleFocus
Open,
ToggleFocus,
NewSearchInDirectory,
]
);
@ -129,8 +135,9 @@ pub fn init_settings(cx: &mut AppContext) {
settings::register::<ProjectPanelSettings>(cx);
}
pub fn init(cx: &mut AppContext) {
pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
init_settings(cx);
file_associations::init(assets, cx);
cx.add_action(ProjectPanel::expand_selected_entry);
cx.add_action(ProjectPanel::collapse_selected_entry);
cx.add_action(ProjectPanel::select_prev);
@ -140,12 +147,14 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectPanel::rename);
cx.add_async_action(ProjectPanel::delete);
cx.add_async_action(ProjectPanel::confirm);
cx.add_async_action(ProjectPanel::open_file);
cx.add_action(ProjectPanel::cancel);
cx.add_action(ProjectPanel::cut);
cx.add_action(ProjectPanel::copy);
cx.add_action(ProjectPanel::copy_path);
cx.add_action(ProjectPanel::copy_relative_path);
cx.add_action(ProjectPanel::reveal_in_finder);
cx.add_action(ProjectPanel::new_search_in_directory);
cx.add_action(
|this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
this.paste(action, cx);
@ -164,6 +173,10 @@ pub enum Event {
},
DockPositionChanged,
Focus,
NewSearchInDirectory {
dir_entry: Entry,
},
ActivatePanel,
}
#[derive(Serialize, Deserialize)]
@ -190,6 +203,9 @@ impl ProjectPanel {
cx.notify();
}
}
project::Event::ActivateProjectPanel => {
cx.emit(Event::ActivatePanel);
}
project::Event::WorktreeRemoved(id) => {
this.expanded_dir_ids.remove(id);
this.update_visible_entries(None, cx);
@ -230,6 +246,11 @@ impl ProjectPanel {
})
.detach();
cx.observe_global::<FileAssociations, _>(|_, cx| {
cx.notify();
})
.detach();
let view_id = cx.view_id();
let mut this = Self {
project: project.clone(),
@ -407,6 +428,12 @@ impl ProjectPanel {
CopyRelativePath,
));
menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
if entry.is_dir() {
menu_entries.push(ContextMenuItem::action(
"Search inside",
NewSearchInDirectory,
));
}
if let Some(clipboard_entry) = self.clipboard_entry {
if clipboard_entry.worktree_id() == worktree.id() {
menu_entries.push(ContextMenuItem::action("Paste", Paste));
@ -535,15 +562,20 @@ impl ProjectPanel {
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
if let Some(task) = self.confirm_edit(cx) {
Some(task)
} else if let Some((_, entry)) = self.selected_entry(cx) {
return Some(task);
}
None
}
fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
if let Some((_, entry)) = self.selected_entry(cx) {
if entry.is_file() {
self.open_entry(entry.id, true, cx);
}
None
} else {
None
}
None
}
fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
@ -720,13 +752,20 @@ impl ProjectPanel {
is_dir: entry.is_dir(),
processing_filename: None,
});
let filename = entry
let file_name = entry
.path
.file_name()
.map_or(String::new(), |s| s.to_string_lossy().to_string());
.map(|s| s.to_string_lossy())
.unwrap_or_default()
.to_string();
let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
let selection_end =
file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
self.filename_editor.update(cx, |editor, cx| {
editor.set_text(filename, cx);
editor.select_all(&Default::default(), cx);
editor.set_text(file_name, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges([0..selection_end])
})
});
cx.focus(&self.filename_editor);
self.update_visible_entries(None, cx);
@ -918,6 +957,20 @@ impl ProjectPanel {
}
}
pub fn new_search_in_directory(
&mut self,
_: &NewSearchInDirectory,
cx: &mut ViewContext<Self>,
) {
if let Some((_, entry)) = self.selected_entry(cx) {
if entry.is_dir() {
cx.emit(Event::NewSearchInDirectory {
dir_entry: entry.clone(),
});
}
}
}
fn move_entry(
&mut self,
entry_to_move: ProjectEntryId,
@ -972,7 +1025,10 @@ impl ProjectPanel {
None
}
fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> {
pub fn selected_entry<'a>(
&self,
cx: &'a AppContext,
) -> Option<(&'a Worktree, &'a project::Entry)> {
let (worktree, entry) = self.selected_entry_handle(cx)?;
Some((worktree.read(cx), entry))
}
@ -1166,7 +1222,14 @@ impl ProjectPanel {
}
let end_ix = range.end.min(ix + visible_worktree_entries.len());
let git_status_setting = settings::get::<ProjectPanelSettings>(cx).git_status;
let (git_status_setting, show_file_icons, show_folder_icons) = {
let settings = settings::get::<ProjectPanelSettings>(cx);
(
settings.git_status,
settings.file_icons,
settings.folder_icons,
)
};
if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
let snapshot = worktree.read(cx).snapshot();
let root_name = OsStr::new(snapshot.root_name());
@ -1179,6 +1242,23 @@ impl ProjectPanel {
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
for entry in visible_worktree_entries[entry_range].iter() {
let status = git_status_setting.then(|| entry.git_status).flatten();
let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
let icon = match entry.kind {
EntryKind::File(_) => {
if show_file_icons {
Some(FileAssociations::get_icon(&entry.path, cx))
} else {
None
}
}
_ => {
if show_folder_icons {
Some(FileAssociations::get_folder_icon(is_expanded, cx))
} else {
Some(FileAssociations::get_chevron_icon(is_expanded, cx))
}
}
};
let mut details = EntryDetails {
filename: entry
@ -1187,11 +1267,12 @@ impl ProjectPanel {
.unwrap_or(root_name)
.to_string_lossy()
.to_string(),
icon,
path: entry.path.clone(),
depth: entry.path.components().count(),
kind: entry.kind,
is_ignored: entry.is_ignored,
is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
is_expanded,
is_selected: self.selection.map_or(false, |e| {
e.worktree_id == snapshot.id() && e.entry_id == entry.id
}),
@ -1239,7 +1320,6 @@ impl ProjectPanel {
style: &ProjectPanelEntry,
cx: &mut ViewContext<V>,
) -> AnyElement<V> {
let kind = details.kind;
let show_editor = details.is_editing && !details.is_processing;
let mut filename_text_style = style.text.clone();
@ -1254,23 +1334,24 @@ impl ProjectPanel {
.unwrap_or(style.text.color);
Flex::row()
.with_child(
if kind.is_dir() {
if details.is_expanded {
Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color)
} else {
Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color)
}
.with_child(if let Some(icon) = &details.icon {
Svg::new(icon.to_string())
.with_color(style.icon_color)
.constrained()
} else {
Empty::new().constrained()
}
.with_max_width(style.icon_size)
.with_max_height(style.icon_size)
.aligned()
.constrained()
.with_width(style.icon_size),
)
.with_max_width(style.icon_size)
.with_max_height(style.icon_size)
.aligned()
.constrained()
.with_width(style.icon_size)
} else {
Empty::new()
.constrained()
.with_max_width(style.icon_size)
.with_max_height(style.icon_size)
.aligned()
.constrained()
.with_width(style.icon_size)
})
.with_child(if show_editor && editor.is_some() {
ChildView::new(editor.as_ref().unwrap(), cx)
.contained()
@ -1305,7 +1386,8 @@ impl ProjectPanel {
) -> AnyElement<Self> {
let kind = details.kind;
let path = details.path.clone();
let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
let settings = settings::get::<ProjectPanelSettings>(cx);
let padding = theme.container.padding.left + details.depth as f32 * settings.indent_size;
let entry_style = if details.is_cut {
&theme.cut_entry
@ -1641,7 +1723,11 @@ mod tests {
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
use std::{collections::HashSet, path::Path};
use std::{
collections::HashSet,
path::Path,
sync::atomic::{self, AtomicUsize},
};
use workspace::{pane, AppState};
#[gpui::test]
@ -1877,7 +1963,7 @@ mod tests {
.update(cx, |panel, cx| {
panel
.filename_editor
.update(cx, |editor, cx| editor.set_text("another-filename", cx));
.update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
panel.confirm(&Confirm, cx).unwrap()
})
.await
@ -1891,14 +1977,14 @@ mod tests {
" v b",
" > 3",
" > 4",
" another-filename <== selected",
" another-filename.txt <== selected",
" > C",
" .dockerignore",
" the-new-filename",
]
);
select_path(&panel, "root1/b/another-filename", cx);
select_path(&panel, "root1/b/another-filename.txt", cx);
panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
@ -1909,7 +1995,7 @@ mod tests {
" v b",
" > 3",
" > 4",
" [EDITOR: 'another-filename'] <== selected",
" [EDITOR: 'another-filename.txt'] <== selected",
" > C",
" .dockerignore",
" the-new-filename",
@ -1917,9 +2003,15 @@ mod tests {
);
let confirm = panel.update(cx, |panel, cx| {
panel
.filename_editor
.update(cx, |editor, cx| editor.set_text("a-different-filename", cx));
panel.filename_editor.update(cx, |editor, cx| {
let file_name_selections = editor.selections.all::<usize>(cx);
assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
let file_name_selection = &file_name_selections[0];
assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
editor.set_text("a-different-filename.tar.gz", cx)
});
panel.confirm(&Confirm, cx).unwrap()
});
assert_eq!(
@ -1931,7 +2023,7 @@ mod tests {
" v b",
" > 3",
" > 4",
" [PROCESSING: 'a-different-filename'] <== selected",
" [PROCESSING: 'a-different-filename.tar.gz'] <== selected",
" > C",
" .dockerignore",
" the-new-filename",
@ -1948,13 +2040,42 @@ mod tests {
" v b",
" > 3",
" > 4",
" a-different-filename <== selected",
" a-different-filename.tar.gz <== selected",
" > C",
" .dockerignore",
" the-new-filename",
]
);
panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v root1",
" > .git",
" > a",
" v b",
" > 3",
" > 4",
" [EDITOR: 'a-different-filename.tar.gz'] <== selected",
" > C",
" .dockerignore",
" the-new-filename",
]
);
panel.update(cx, |panel, cx| {
panel.filename_editor.update(cx, |editor, cx| {
let file_name_selections = editor.selections.all::<usize>(cx);
assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
let file_name_selection = &file_name_selections[0];
assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot");
});
panel.cancel(&Cancel, cx)
});
panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
@ -1966,7 +2087,7 @@ mod tests {
" > [EDITOR: ''] <== selected",
" > 3",
" > 4",
" a-different-filename",
" a-different-filename.tar.gz",
" > C",
" .dockerignore",
]
@ -1989,7 +2110,7 @@ mod tests {
" > [PROCESSING: 'new-dir']",
" > 3 <== selected",
" > 4",
" a-different-filename",
" a-different-filename.tar.gz",
" > C",
" .dockerignore",
]
@ -2006,7 +2127,7 @@ mod tests {
" > 3 <== selected",
" > 4",
" > new-dir",
" a-different-filename",
" a-different-filename.tar.gz",
" > C",
" .dockerignore",
]
@ -2023,7 +2144,7 @@ mod tests {
" > [EDITOR: '3'] <== selected",
" > 4",
" > new-dir",
" a-different-filename",
" a-different-filename.tar.gz",
" > C",
" .dockerignore",
]
@ -2041,7 +2162,7 @@ mod tests {
" > 3 <== selected",
" > 4",
" > new-dir",
" a-different-filename",
" a-different-filename.tar.gz",
" > C",
" .dockerignore",
]
@ -2268,7 +2389,7 @@ mod tests {
toggle_expand_dir(&panel, "src/test", cx);
select_path(&panel, "src/test/first.rs", cx);
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
cx.foreground().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
@ -2296,7 +2417,7 @@ mod tests {
ensure_no_open_items_and_panes(window_id, &workspace, cx);
select_path(&panel, "src/test/second.rs", cx);
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
cx.foreground().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
@ -2480,6 +2601,83 @@ mod tests {
);
}
#[gpui::test]
async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/src",
json!({
"test": {
"first.rs": "// First Rust file",
"second.rs": "// Second Rust file",
"third.rs": "// Third Rust file",
}
}),
)
.await;
let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
let new_search_events_count = Arc::new(AtomicUsize::new(0));
let _subscription = panel.update(cx, |_, cx| {
let subcription_count = Arc::clone(&new_search_events_count);
cx.subscribe(&cx.handle(), move |_, _, event, _| {
if matches!(event, Event::NewSearchInDirectory { .. }) {
subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
}
})
});
toggle_expand_dir(&panel, "src/test", cx);
select_path(&panel, "src/test/first.rs", cx);
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
cx.foreground().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v src",
" v test",
" first.rs <== selected",
" second.rs",
" third.rs"
]
);
panel.update(cx, |panel, cx| {
panel.new_search_in_directory(&NewSearchInDirectory, cx)
});
assert_eq!(
new_search_events_count.load(atomic::Ordering::SeqCst),
0,
"Should not trigger new search in directory when called on a file"
);
select_path(&panel, "src/test", cx);
panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
cx.foreground().run_until_parked();
assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx),
&[
"v src",
" v test <== selected",
" first.rs",
" second.rs",
" third.rs"
]
);
panel.update(cx, |panel, cx| {
panel.new_search_in_directory(&NewSearchInDirectory, cx)
});
assert_eq!(
new_search_events_count.load(atomic::Ordering::SeqCst),
1,
"Should trigger new search in directory when called on a directory"
);
}
fn toggle_expand_dir(
panel: &ViewHandle<ProjectPanel>,
path: impl AsRef<Path>,
@ -2581,7 +2779,7 @@ mod tests {
theme::init((), cx);
language::init(cx);
editor::init_settings(cx);
crate::init(cx);
crate::init((), cx);
workspace::init_settings(cx);
Project::init_settings(cx);
});
@ -2596,7 +2794,7 @@ mod tests {
language::init(cx);
editor::init(cx);
pane::init(cx);
crate::init(cx);
crate::init((), cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
});

View File

@ -12,16 +12,22 @@ pub enum ProjectPanelDockPosition {
#[derive(Deserialize, Debug)]
pub struct ProjectPanelSettings {
pub git_status: bool,
pub dock: ProjectPanelDockPosition,
pub default_width: f32,
pub dock: ProjectPanelDockPosition,
pub file_icons: bool,
pub folder_icons: bool,
pub git_status: bool,
pub indent_size: f32,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ProjectPanelSettingsContent {
pub git_status: Option<bool>,
pub dock: Option<ProjectPanelDockPosition>,
pub default_width: Option<f32>,
pub dock: Option<ProjectPanelDockPosition>,
pub file_icons: Option<bool>,
pub folder_icons: Option<bool>,
pub git_status: Option<bool>,
pub indent_size: Option<f32>,
}
impl Setting for ProjectPanelSettings {

View File

@ -9,6 +9,7 @@ path = "src/search.rs"
doctest = false
[dependencies]
bitflags = "1"
collections = { path = "../collections" }
editor = { path = "../editor" }
gpui = { path = "../gpui" }

View File

@ -1,15 +1,17 @@
use crate::{
SearchOption, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive,
ToggleRegex, ToggleWholeWord,
};
use collections::HashMap;
use editor::Editor;
use futures::channel::oneshot;
use gpui::{
actions,
elements::*,
impl_actions,
platform::{CursorStyle, MouseButton},
Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle,
WindowContext,
};
use project::search::SearchQuery;
use serde::Deserialize;
@ -44,20 +46,19 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(BufferSearchBar::select_prev_match_on_pane);
cx.add_action(BufferSearchBar::select_all_matches_on_pane);
cx.add_action(BufferSearchBar::handle_editor_cancel);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
}
fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) {
search_bar.update(cx, |search_bar, cx| {
search_bar.update(cx, |search_bar, cx| {
if search_bar.show(cx) {
search_bar.toggle_search_option(option, cx);
});
return;
}
}
});
}
cx.propagate_action();
});
@ -71,9 +72,8 @@ pub struct BufferSearchBar {
searchable_items_with_matches:
HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
pending_search: Option<Task<()>>,
case_sensitive: bool,
whole_word: bool,
regex: bool,
search_options: SearchOptions,
default_options: SearchOptions,
query_contains_error: bool,
dismissed: bool,
}
@ -156,19 +156,19 @@ impl View for BufferSearchBar {
.with_children(self.render_search_option(
supported_options.case,
"Case",
SearchOption::CaseSensitive,
SearchOptions::CASE_SENSITIVE,
cx,
))
.with_children(self.render_search_option(
supported_options.word,
"Word",
SearchOption::WholeWord,
SearchOptions::WHOLE_WORD,
cx,
))
.with_children(self.render_search_option(
supported_options.regex,
"Regex",
SearchOption::Regex,
SearchOptions::REGEX,
cx,
))
.contained()
@ -212,7 +212,7 @@ impl ToolbarItemView for BufferSearchBar {
));
self.active_searchable_item = Some(searchable_item_handle);
self.update_matches(false, cx);
let _ = self.update_matches(cx);
if !self.dismissed {
return ToolbarItemLocation::Secondary;
}
@ -253,9 +253,8 @@ impl BufferSearchBar {
active_searchable_item_subscription: None,
active_match_index: None,
searchable_items_with_matches: Default::default(),
case_sensitive: false,
whole_word: false,
regex: false,
default_options: SearchOptions::NONE,
search_options: SearchOptions::NONE,
pending_search: None,
query_contains_error: false,
dismissed: true,
@ -282,48 +281,86 @@ impl BufferSearchBar {
cx.notify();
}
pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext<Self>) -> bool {
let searchable_item = if let Some(searchable_item) = &self.active_searchable_item {
SearchableItemHandle::boxed_clone(searchable_item.as_ref())
} else {
pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
if self.active_searchable_item.is_none() {
return false;
};
if suggest_query {
let text = searchable_item.query_suggestion(cx);
if !text.is_empty() {
self.set_query(&text, cx);
}
}
if focus {
let query_editor = self.query_editor.clone();
query_editor.update(cx, |query_editor, cx| {
query_editor.select_all(&editor::SelectAll, cx);
});
cx.focus_self();
}
self.dismissed = false;
cx.notify();
cx.emit(Event::UpdateLocation);
true
}
fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
let search = self
.query_suggestion(cx)
.map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
if let Some(search) = search {
cx.spawn(|this, mut cx| async move {
search.await?;
this.update(&mut cx, |this, cx| this.activate_current_match(cx))
})
.detach_and_log_err(cx);
}
}
pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
if let Some(match_ix) = self.active_match_index {
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
.searchable_items_with_matches
.get(&active_searchable_item.downgrade())
{
active_searchable_item.activate_match(match_ix, matches, cx)
}
}
}
}
pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.buffer().update(cx, |query_buffer, cx| {
let len = query_buffer.len(cx);
query_buffer.edit([(0..len, query)], None, cx);
});
query_editor.select_all(&Default::default(), cx);
});
}
pub fn query(&self, cx: &WindowContext) -> String {
self.query_editor.read(cx).text(cx)
}
pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
self.active_searchable_item
.as_ref()
.map(|searchable_item| searchable_item.query_suggestion(cx))
}
pub fn search(
&mut self,
query: &str,
options: Option<SearchOptions>,
cx: &mut ViewContext<Self>,
) -> oneshot::Receiver<()> {
let options = options.unwrap_or(self.default_options);
if query != self.query_editor.read(cx).text(cx) || self.search_options != options {
self.query_editor.update(cx, |query_editor, cx| {
query_editor.buffer().update(cx, |query_buffer, cx| {
let len = query_buffer.len(cx);
query_buffer.edit([(0..len, query)], None, cx);
});
});
self.search_options = options;
self.query_contains_error = false;
self.clear_matches(cx);
cx.notify();
}
self.update_matches(cx)
}
fn render_search_option(
&self,
option_supported: bool,
icon: &'static str,
option: SearchOption,
option: SearchOptions,
cx: &mut ViewContext<Self>,
) -> Option<AnyElement<Self>> {
if !option_supported {
@ -331,9 +368,9 @@ impl BufferSearchBar {
}
let tooltip_style = theme::current(cx).tooltip.clone();
let is_active = self.is_search_option_enabled(option);
let is_active = self.search_options.contains(option);
Some(
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.search
@ -349,7 +386,7 @@ impl BufferSearchBar {
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<Self>(
option as usize,
option.bits as usize,
format!("Toggle {}", option.label()),
Some(option.to_toggle_action()),
tooltip_style,
@ -471,12 +508,23 @@ impl BufferSearchBar {
}
fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext<Pane>) {
let mut propagate_action = true;
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) {
return;
}
search_bar.update(cx, |search_bar, cx| {
if search_bar.show(cx) {
search_bar.search_suggested(cx);
if action.focus {
search_bar.select_query(cx);
cx.focus_self();
}
propagate_action = false;
}
});
}
if propagate_action {
cx.propagate_action();
}
cx.propagate_action();
}
fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext<Pane>) {
@ -489,41 +537,38 @@ impl BufferSearchBar {
cx.propagate_action();
}
fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
if let Some(active_editor) = self.active_searchable_item.as_ref() {
cx.focus(active_editor.as_any());
}
}
fn is_search_option_enabled(&self, search_option: SearchOption) -> bool {
match search_option {
SearchOption::WholeWord => self.whole_word,
SearchOption::CaseSensitive => self.case_sensitive,
SearchOption::Regex => self.regex,
}
fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
self.search_options.toggle(search_option);
self.default_options = self.search_options;
let _ = self.update_matches(cx);
cx.notify();
}
fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext<Self>) {
let value = match search_option {
SearchOption::WholeWord => &mut self.whole_word,
SearchOption::CaseSensitive => &mut self.case_sensitive,
SearchOption::Regex => &mut self.regex,
};
*value = !*value;
self.update_matches(false, cx);
pub fn set_search_options(
&mut self,
search_options: SearchOptions,
cx: &mut ViewContext<Self>,
) {
self.search_options = search_options;
cx.notify();
}
fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
self.select_match(Direction::Next, cx);
self.select_match(Direction::Next, 1, cx);
}
fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
self.select_match(Direction::Prev, cx);
self.select_match(Direction::Prev, 1, cx);
}
fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
if !self.dismissed {
if !self.dismissed && self.active_match_index.is_some() {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
.searchable_items_with_matches
@ -536,15 +581,15 @@ impl BufferSearchBar {
}
}
pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
if let Some(index) = self.active_match_index {
if let Some(searchable_item) = self.active_searchable_item.as_ref() {
if let Some(matches) = self
.searchable_items_with_matches
.get(&searchable_item.downgrade())
{
let new_match_index =
searchable_item.match_index_for_direction(matches, index, direction, cx);
let new_match_index = searchable_item
.match_index_for_direction(matches, index, direction, count, cx);
searchable_item.update_matches(matches, cx);
searchable_item.activate_match(new_match_index, matches, cx);
}
@ -588,17 +633,23 @@ impl BufferSearchBar {
event: &editor::Event,
cx: &mut ViewContext<Self>,
) {
if let editor::Event::BufferEdited { .. } = event {
if let editor::Event::Edited { .. } = event {
self.query_contains_error = false;
self.clear_matches(cx);
self.update_matches(true, cx);
cx.notify();
let search = self.update_matches(cx);
cx.spawn(|this, mut cx| async move {
search.await?;
this.update(&mut cx, |this, cx| this.activate_current_match(cx))
})
.detach_and_log_err(cx);
}
}
fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext<Self>) {
match event {
SearchEvent::MatchesInvalidated => self.update_matches(false, cx),
SearchEvent::MatchesInvalidated => {
let _ = self.update_matches(cx);
}
SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
}
}
@ -621,19 +672,21 @@ impl BufferSearchBar {
.extend(active_item_matches);
}
fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext<Self>) {
fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
let (done_tx, done_rx) = oneshot::channel();
let query = self.query_editor.read(cx).text(cx);
self.pending_search.take();
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
if query.is_empty() {
self.active_match_index.take();
active_searchable_item.clear_matches(cx);
let _ = done_tx.send(());
} else {
let query = if self.regex {
let query = if self.search_options.contains(SearchOptions::REGEX) {
match SearchQuery::regex(
query,
self.whole_word,
self.case_sensitive,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
Vec::new(),
Vec::new(),
) {
@ -641,14 +694,14 @@ impl BufferSearchBar {
Err(_) => {
self.query_contains_error = true;
cx.notify();
return;
return done_rx;
}
}
} else {
SearchQuery::text(
query,
self.whole_word,
self.case_sensitive,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
Vec::new(),
Vec::new(),
)
@ -673,12 +726,7 @@ impl BufferSearchBar {
.get(&active_searchable_item.downgrade())
.unwrap();
active_searchable_item.update_matches(matches, cx);
if select_closest_match {
if let Some(match_ix) = this.active_match_index {
active_searchable_item
.activate_match(match_ix, matches, cx);
}
}
let _ = done_tx.send(());
}
cx.notify();
}
@ -687,6 +735,7 @@ impl BufferSearchBar {
}));
}
}
done_rx
}
fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
@ -714,8 +763,7 @@ mod tests {
use language::Buffer;
use unindent::Unindent as _;
#[gpui::test]
async fn test_search_simple(cx: &mut TestAppContext) {
fn init_test(cx: &mut TestAppContext) -> (ViewHandle<Editor>, ViewHandle<BufferSearchBar>) {
crate::project_search::tests::init_test(cx);
let buffer = cx.add_model(|cx| {
@ -738,16 +786,23 @@ mod tests {
let search_bar = cx.add_view(window_id, |cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(false, true, cx);
search_bar.show(cx);
search_bar
});
(editor, search_bar)
}
#[gpui::test]
async fn test_search_simple(cx: &mut TestAppContext) {
let (editor, search_bar) = init_test(cx);
// Search for a string that appears with different casing.
// By default, search is case-insensitive.
search_bar.update(cx, |search_bar, cx| {
search_bar.set_query("us", cx);
});
editor.next_notification(cx).await;
search_bar
.update(cx, |search_bar, cx| search_bar.search("us", None, cx))
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
@ -766,7 +821,7 @@ mod tests {
// Switch to a case sensitive search.
search_bar.update(cx, |search_bar, cx| {
search_bar.toggle_search_option(SearchOption::CaseSensitive, cx);
search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
});
editor.next_notification(cx).await;
editor.update(cx, |editor, cx| {
@ -781,10 +836,10 @@ mod tests {
// Search for a string that appears both as a whole word and
// within other words. By default, all results are found.
search_bar.update(cx, |search_bar, cx| {
search_bar.set_query("or", cx);
});
editor.next_notification(cx).await;
search_bar
.update(cx, |search_bar, cx| search_bar.search("or", None, cx))
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
@ -823,7 +878,7 @@ mod tests {
// Switch to a whole word search.
search_bar.update(cx, |search_bar, cx| {
search_bar.toggle_search_option(SearchOption::WholeWord, cx);
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
});
editor.next_notification(cx).await;
editor.update(cx, |editor, cx| {
@ -1025,6 +1080,65 @@ mod tests {
});
}
#[gpui::test]
async fn test_search_option_handling(cx: &mut TestAppContext) {
let (editor, search_bar) = init_test(cx);
// show with options should make current search case sensitive
search_bar
.update(cx, |search_bar, cx| {
search_bar.show(cx);
search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
})
.await
.unwrap();
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
&[(
DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
Color::red(),
)]
);
});
// search_suggested should restore default options
search_bar.update(cx, |search_bar, cx| {
search_bar.search_suggested(cx);
assert_eq!(search_bar.search_options, SearchOptions::NONE)
});
// toggling a search option should update the defaults
search_bar
.update(cx, |search_bar, cx| {
search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
})
.await
.unwrap();
search_bar.update(cx, |search_bar, cx| {
search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
});
editor.next_notification(cx).await;
editor.update(cx, |editor, cx| {
assert_eq!(
editor.all_background_highlights(cx),
&[(
DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),
Color::red(),
),]
);
});
// defaults should still include whole word
search_bar.update(cx, |search_bar, cx| {
search_bar.search_suggested(cx);
assert_eq!(
search_bar.search_options,
SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
)
});
}
#[gpui::test]
async fn test_search_select_all_matches(cx: &mut TestAppContext) {
crate::project_search::tests::init_test(cx);
@ -1052,15 +1166,25 @@ mod tests {
let search_bar = cx.add_view(window_id, |cx| {
let mut search_bar = BufferSearchBar::new(cx);
search_bar.set_active_pane_item(Some(&editor), cx);
search_bar.show(false, true, cx);
search_bar.show(cx);
search_bar
});
search_bar
.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
.await
.unwrap();
search_bar.update(cx, |search_bar, cx| {
search_bar.set_query("a", cx);
cx.focus(search_bar.query_editor.as_any());
search_bar.activate_current_match(cx);
});
editor.next_notification(cx).await;
cx.read_window(window_id, |cx| {
assert!(
!editor.is_focused(cx),
"Initially, the editor should not be focused"
);
});
let initial_selections = editor.update(cx, |editor, cx| {
let initial_selections = editor.selections.display_ranges(cx);
assert_eq!(
@ -1074,7 +1198,16 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
cx.focus(search_bar.query_editor.as_any());
search_bar.select_all_matches(&SelectAllMatches, cx);
});
cx.read_window(window_id, |cx| {
assert!(
editor.is_focused(cx),
"Should focus editor after successful SelectAllMatches"
);
});
search_bar.update(cx, |search_bar, cx| {
let all_selections =
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
assert_eq!(
@ -1082,8 +1215,6 @@ mod tests {
expected_query_matches_count,
"Should select all `a` characters in the buffer, but got: {all_selections:?}"
);
});
search_bar.update(cx, |search_bar, _| {
assert_eq!(
search_bar.active_match_index,
Some(0),
@ -1093,6 +1224,14 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_next_match(&SelectNextMatch, cx);
});
cx.read_window(window_id, |cx| {
assert!(
editor.is_focused(cx),
"Should still have editor focused after SelectNextMatch"
);
});
search_bar.update(cx, |search_bar, cx| {
let all_selections =
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
assert_eq!(
@ -1104,8 +1243,6 @@ mod tests {
all_selections, initial_selections,
"Next match should be different from the first selection"
);
});
search_bar.update(cx, |search_bar, _| {
assert_eq!(
search_bar.active_match_index,
Some(1),
@ -1114,7 +1251,16 @@ mod tests {
});
search_bar.update(cx, |search_bar, cx| {
cx.focus(search_bar.query_editor.as_any());
search_bar.select_all_matches(&SelectAllMatches, cx);
});
cx.read_window(window_id, |cx| {
assert!(
editor.is_focused(cx),
"Should focus editor after successful SelectAllMatches"
);
});
search_bar.update(cx, |search_bar, cx| {
let all_selections =
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
assert_eq!(
@ -1122,8 +1268,6 @@ mod tests {
expected_query_matches_count,
"Should select all `a` characters in the buffer, but got: {all_selections:?}"
);
});
search_bar.update(cx, |search_bar, _| {
assert_eq!(
search_bar.active_match_index,
Some(1),
@ -1133,6 +1277,14 @@ mod tests {
search_bar.update(cx, |search_bar, cx| {
search_bar.select_prev_match(&SelectPrevMatch, cx);
});
cx.read_window(window_id, |cx| {
assert!(
editor.is_focused(cx),
"Should still have editor focused after SelectPrevMatch"
);
});
let last_match_selections = search_bar.update(cx, |search_bar, cx| {
let all_selections =
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
assert_eq!(
@ -1144,13 +1296,41 @@ mod tests {
all_selections, initial_selections,
"Previous match should be the same as the first selection"
);
});
search_bar.update(cx, |search_bar, _| {
assert_eq!(
search_bar.active_match_index,
Some(0),
"Match index should be updated to the previous one"
);
all_selections
});
search_bar
.update(cx, |search_bar, cx| {
cx.focus(search_bar.query_editor.as_any());
search_bar.search("abas_nonexistent_match", None, cx)
})
.await
.unwrap();
search_bar.update(cx, |search_bar, cx| {
search_bar.select_all_matches(&SelectAllMatches, cx);
});
cx.read_window(window_id, |cx| {
assert!(
!editor.is_focused(cx),
"Should not switch focus to editor if SelectAllMatches does not find any matches"
);
});
search_bar.update(cx, |search_bar, cx| {
let all_selections =
editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
assert_eq!(
all_selections, last_match_selections,
"Should not select anything new if there are no matches"
);
assert!(
search_bar.active_match_index.is_none(),
"For no matches, there should be no active match index"
);
});
}
}

View File

@ -1,5 +1,5 @@
use crate::{
SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex,
ToggleWholeWord,
};
use anyhow::Result;
@ -19,7 +19,7 @@ use gpui::{
};
use menu::Confirm;
use postage::stream::Stream;
use project::{search::SearchQuery, Project};
use project::{search::SearchQuery, Entry, Project};
use semantic_index::SemanticIndex;
use smallvec::SmallVec;
use std::{
@ -56,12 +56,12 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(ProjectSearchBar::select_prev_match);
cx.capture_action(ProjectSearchBar::tab);
cx.capture_action(ProjectSearchBar::tab_previous);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOption::CaseSensitive, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOption::WholeWord, cx);
add_toggle_option_action::<ToggleRegex>(SearchOption::Regex, cx);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
add_toggle_option_action::<ToggleRegex>(SearchOptions::REGEX, cx);
}
fn add_toggle_option_action<A: Action>(option: SearchOption, cx: &mut AppContext) {
fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
if search_bar.update(cx, |search_bar, cx| {
@ -94,10 +94,8 @@ pub struct ProjectSearchView {
model: ModelHandle<ProjectSearch>,
query_editor: ViewHandle<Editor>,
results_editor: ViewHandle<Editor>,
case_sensitive: bool,
whole_word: bool,
regex: bool,
semantic: Option<SemanticSearchState>,
search_options: SearchOptions,
panels_with_errors: HashSet<InputPanel>,
active_match_index: Option<usize>,
search_id: usize,
@ -488,9 +486,7 @@ impl ProjectSearchView {
let project;
let excerpts;
let mut query_text = String::new();
let mut regex = false;
let mut case_sensitive = false;
let mut whole_word = false;
let mut options = SearchOptions::NONE;
{
let model = model.read(cx);
@ -498,9 +494,7 @@ impl ProjectSearchView {
excerpts = model.excerpts.clone();
if let Some(active_query) = model.active_query.as_ref() {
query_text = active_query.as_str().to_string();
regex = active_query.is_regex();
case_sensitive = active_query.case_sensitive();
whole_word = active_query.whole_word();
options = SearchOptions::from_query(active_query);
}
}
cx.observe(&model, |this, _, cx| this.model_changed(cx))
@ -576,10 +570,8 @@ impl ProjectSearchView {
model,
query_editor,
results_editor,
case_sensitive,
whole_word,
regex,
semantic: None,
search_options: options,
panels_with_errors: HashSet::new(),
active_match_index: None,
query_editor_was_focused: false,
@ -590,6 +582,28 @@ impl ProjectSearchView {
this
}
pub fn new_search_in_directory(
workspace: &mut Workspace,
dir_entry: &Entry,
cx: &mut ViewContext<Workspace>,
) {
if !dir_entry.is_dir() {
return;
}
let filter_path = dir_entry.path.join("**");
let Some(filter_str) = filter_path.to_str() else { return; };
let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
let search = cx.add_view(|cx| ProjectSearchView::new(model, cx));
workspace.add_item(Box::new(search.clone()), cx);
search.update(cx, |search, cx| {
search
.included_files_editor
.update(cx, |editor, cx| editor.set_text(filter_str, cx));
search.focus_query_editor(cx)
});
}
// Re-activate the most recently activated search or the most recent if it has been closed.
// If no search exists in the workspace, create a new one.
fn deploy(
@ -723,11 +737,11 @@ impl ProjectSearchView {
return None;
}
};
if self.regex {
if self.search_options.contains(SearchOptions::REGEX) {
match SearchQuery::regex(
text,
self.whole_word,
self.case_sensitive,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
included_files,
excluded_files,
) {
@ -744,8 +758,8 @@ impl ProjectSearchView {
} else {
Some(SearchQuery::text(
text,
self.whole_word,
self.case_sensitive,
self.search_options.contains(SearchOptions::WHOLE_WORD),
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
included_files,
excluded_files,
))
@ -764,7 +778,7 @@ impl ProjectSearchView {
if let Some(index) = self.active_match_index {
let match_ranges = self.model.read(cx).match_ranges.clone();
let new_index = self.results_editor.update(cx, |editor, cx| {
editor.match_index_for_direction(&match_ranges, index, direction, cx)
editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
});
let range_to_select = match_ranges[new_index].clone();
@ -805,7 +819,6 @@ impl ProjectSearchView {
self.active_match_index = None;
} else {
self.active_match_index = Some(0);
self.select_match(Direction::Next, cx);
self.update_match_index(cx);
let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
let is_new_search = self.search_id != prev_search_id;
@ -897,9 +910,7 @@ impl ProjectSearchBar {
search_view.query_editor.update(cx, |editor, cx| {
editor.set_text(old_query.as_str(), cx);
});
search_view.regex = old_query.is_regex();
search_view.whole_word = old_query.whole_word();
search_view.case_sensitive = old_query.case_sensitive();
search_view.search_options = SearchOptions::from_query(&old_query);
}
}
new_query
@ -987,19 +998,11 @@ impl ProjectSearchBar {
});
}
fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext<Self>) -> bool {
fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
if let Some(search_view) = self.active_project_search.as_ref() {
search_view.update(cx, |search_view, cx| {
let value = match option {
SearchOption::WholeWord => &mut search_view.whole_word,
SearchOption::CaseSensitive => &mut search_view.case_sensitive,
SearchOption::Regex => &mut search_view.regex,
};
*value = !*value;
if value.clone() {
search_view.semantic = None;
}
search_view.search_options.toggle(option);
search_view.semantic = None;
search_view.search(cx);
});
cx.notify();
@ -1016,9 +1019,7 @@ impl ProjectSearchBar {
search_view.semantic = None;
} else if let Some(semantic_index) = SemanticIndex::global(cx) {
// TODO: confirm that it's ok to send this project
search_view.regex = false;
search_view.case_sensitive = false;
search_view.whole_word = false;
search_view.search_options = SearchOptions::none();
let project = search_view.model.read(cx).project.clone();
let index_task = semantic_index.update(cx, |semantic_index, cx| {
@ -1113,12 +1114,12 @@ impl ProjectSearchBar {
fn render_option_button(
&self,
icon: &'static str,
option: SearchOption,
option: SearchOptions,
cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
let tooltip_style = theme::current(cx).tooltip.clone();
let is_active = self.is_option_enabled(option, cx);
MouseEventHandler::<Self, _>::new(option as usize, cx, |state, cx| {
MouseEventHandler::<Self, _>::new(option.bits as usize, cx, |state, cx| {
let theme = theme::current(cx);
let style = theme
.search
@ -1134,7 +1135,7 @@ impl ProjectSearchBar {
})
.with_cursor_style(CursorStyle::PointingHand)
.with_tooltip::<Self>(
option as usize,
option.bits as usize,
format!("Toggle {}", option.label()),
Some(option.to_toggle_action()),
tooltip_style,
@ -1179,14 +1180,9 @@ impl ProjectSearchBar {
.into_any()
}
fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool {
fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
if let Some(search) = self.active_project_search.as_ref() {
let search = search.read(cx);
match option {
SearchOption::WholeWord => search.whole_word,
SearchOption::CaseSensitive => search.case_sensitive,
SearchOption::Regex => search.regex,
}
search.read(cx).search_options.contains(option)
} else {
false
}
@ -1283,17 +1279,17 @@ impl View for ProjectSearchBar {
let row = row
.with_child(self.render_option_button(
"Case",
SearchOption::CaseSensitive,
SearchOptions::CASE_SENSITIVE,
cx,
))
.with_child(self.render_option_button(
"Word",
SearchOption::WholeWord,
SearchOptions::WHOLE_WORD,
cx,
))
.with_child(self.render_option_button(
"Regex",
SearchOption::Regex,
SearchOptions::REGEX,
cx,
))
.contained()
@ -1669,6 +1665,134 @@ pub mod tests {
});
}
#[gpui::test]
async fn test_new_project_search_in_directory(
deterministic: Arc<Deterministic>,
cx: &mut TestAppContext,
) {
init_test(cx);
let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/dir",
json!({
"a": {
"one.rs": "const ONE: usize = 1;",
"two.rs": "const TWO: usize = one::ONE + one::ONE;",
},
"b": {
"three.rs": "const THREE: usize = one::ONE + two::TWO;",
"four.rs": "const FOUR: usize = one::ONE + three::THREE;",
},
}),
)
.await;
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
let worktree_id = project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
});
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let active_item = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
});
assert!(
active_item.is_none(),
"Expected no search panel to be active, but got: {active_item:?}"
);
let one_file_entry = cx.update(|cx| {
workspace
.read(cx)
.project()
.read(cx)
.entry_for_path(&(worktree_id, "a/one.rs").into(), cx)
.expect("no entry for /a/one.rs file")
});
assert!(one_file_entry.is_file());
workspace.update(cx, |workspace, cx| {
ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx)
});
let active_search_entry = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
});
assert!(
active_search_entry.is_none(),
"Expected no search panel to be active for file entry"
);
let a_dir_entry = cx.update(|cx| {
workspace
.read(cx)
.project()
.read(cx)
.entry_for_path(&(worktree_id, "a").into(), cx)
.expect("no entry for /a/ directory")
});
assert!(a_dir_entry.is_dir());
workspace.update(cx, |workspace, cx| {
ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx)
});
let Some(search_view) = cx.read(|cx| {
workspace
.read(cx)
.active_pane()
.read(cx)
.active_item()
.and_then(|item| item.downcast::<ProjectSearchView>())
}) else {
panic!("Search view expected to appear after new search in directory event trigger")
};
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
assert!(
search_view.query_editor.is_focused(cx),
"On new search in directory, focus should be moved into query editor"
);
search_view.excluded_files_editor.update(cx, |editor, cx| {
assert!(
editor.display_text(cx).is_empty(),
"New search in directory should not have any excluded files"
);
});
search_view.included_files_editor.update(cx, |editor, cx| {
assert_eq!(
editor.display_text(cx),
a_dir_entry.path.join("**").display().to_string(),
"New search in directory should have included dir entry path"
);
});
});
search_view.update(cx, |search_view, cx| {
search_view
.query_editor
.update(cx, |query_editor, cx| query_editor.set_text("const", cx));
search_view.search(cx);
});
deterministic.run_until_parked();
search_view.update(cx, |search_view, cx| {
assert_eq!(
search_view
.results_editor
.update(cx, |editor, cx| editor.display_text(cx)),
"\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
"New search in directory should have a filter that matches a certain directory"
);
});
}
pub fn init_test(cx: &mut TestAppContext) {
cx.foreground().forbid_parking();
let fonts = cx.font_cache();

View File

@ -1,5 +1,7 @@
use bitflags::bitflags;
pub use buffer_search::BufferSearchBar;
use gpui::{actions, Action, AppContext};
use project::search::SearchQuery;
pub use project_search::{ProjectSearchBar, ProjectSearchView};
pub mod buffer_search;
@ -22,27 +24,44 @@ actions!(
]
);
#[derive(Clone, Copy, PartialEq)]
pub enum SearchOption {
WholeWord,
CaseSensitive,
Regex,
bitflags! {
#[derive(Default)]
pub struct SearchOptions: u8 {
const NONE = 0b000;
const WHOLE_WORD = 0b001;
const CASE_SENSITIVE = 0b010;
const REGEX = 0b100;
}
}
impl SearchOption {
impl SearchOptions {
pub fn label(&self) -> &'static str {
match self {
SearchOption::WholeWord => "Match Whole Word",
SearchOption::CaseSensitive => "Match Case",
SearchOption::Regex => "Use Regular Expression",
match *self {
SearchOptions::WHOLE_WORD => "Match Whole Word",
SearchOptions::CASE_SENSITIVE => "Match Case",
SearchOptions::REGEX => "Use Regular Expression",
_ => panic!("{:?} is not a named SearchOption", self),
}
}
pub fn to_toggle_action(&self) -> Box<dyn Action> {
match self {
SearchOption::WholeWord => Box::new(ToggleWholeWord),
SearchOption::CaseSensitive => Box::new(ToggleCaseSensitive),
SearchOption::Regex => Box::new(ToggleRegex),
match *self {
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
SearchOptions::REGEX => Box::new(ToggleRegex),
_ => panic!("{:?} is not a named SearchOption", self),
}
}
pub fn none() -> SearchOptions {
SearchOptions::NONE
}
pub fn from_query(query: &SearchQuery) -> SearchOptions {
let mut options = SearchOptions::NONE;
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
options.set(SearchOptions::REGEX, query.is_regex());
options
}
}

View File

@ -51,7 +51,7 @@ use gpui::{
fonts,
geometry::vector::{vec2f, Vector2F},
keymap_matcher::Keystroke,
platform::{MouseButton, MouseMovedEvent, TouchPhase},
platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase},
scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp},
AppContext, ClipboardItem, Entity, ModelContext, Task,
};
@ -72,14 +72,17 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.;
const DEBUG_CELL_WIDTH: f32 = 5.;
const DEBUG_LINE_HEIGHT: f32 = 5.;
// Regex Copied from alacritty's ui_config.rs
lazy_static! {
static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap();
// Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
// * avoid Rust-specific escaping.
// * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-~]+").unwrap();
}
///Upward flowing events, for changing the title and such
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub enum Event {
TitleChanged,
BreadcrumbsChanged,
@ -88,6 +91,18 @@ pub enum Event {
Wakeup,
BlinkChanged,
SelectionsChanged,
NewNavigationTarget(Option<MaybeNavigationTarget>),
Open(MaybeNavigationTarget),
}
/// A string inside terminal, potentially useful as a URI that can be opened.
#[derive(Clone, Debug)]
pub enum MaybeNavigationTarget {
/// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.
Url(String),
/// File system path, absolute or relative, existing or not.
/// Might have line and column number(s) attached as `file.rs:1:23`
PathLike(String),
}
#[derive(Clone)]
@ -493,6 +508,8 @@ impl TerminalBuilder {
last_mouse_position: None,
next_link_id: 0,
selection_phase: SelectionPhase::Ended,
cmd_pressed: false,
hovered_word: false,
};
Ok(TerminalBuilder {
@ -589,7 +606,14 @@ pub struct TerminalContent {
pub cursor: RenderableCursor,
pub cursor_char: char,
pub size: TerminalSize,
pub last_hovered_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
pub last_hovered_word: Option<HoveredWord>,
}
#[derive(Clone)]
pub struct HoveredWord {
pub word: String,
pub word_match: RangeInclusive<Point>,
pub id: usize,
}
impl Default for TerminalContent {
@ -606,7 +630,7 @@ impl Default for TerminalContent {
},
cursor_char: Default::default(),
size: Default::default(),
last_hovered_hyperlink: None,
last_hovered_word: None,
}
}
}
@ -623,7 +647,7 @@ pub struct Terminal {
events: VecDeque<InternalEvent>,
/// This is only used for mouse mode cell change detection
last_mouse: Option<(Point, AlacDirection)>,
/// This is only used for terminal hyperlink checking
/// This is only used for terminal hovered word checking
last_mouse_position: Option<Vector2F>,
pub matches: Vec<RangeInclusive<Point>>,
pub last_content: TerminalContent,
@ -637,6 +661,8 @@ pub struct Terminal {
scroll_px: f32,
next_link_id: usize,
selection_phase: SelectionPhase,
cmd_pressed: bool,
hovered_word: bool,
}
impl Terminal {
@ -769,7 +795,7 @@ impl Terminal {
}
InternalEvent::Scroll(scroll) => {
term.scroll_display(*scroll);
self.refresh_hyperlink();
self.refresh_hovered_word();
}
InternalEvent::SetSelection(selection) => {
term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
@ -804,20 +830,20 @@ impl Terminal {
}
InternalEvent::ScrollToPoint(point) => {
term.scroll_to_point(*point);
self.refresh_hyperlink();
self.refresh_hovered_word();
}
InternalEvent::FindHyperlink(position, open) => {
let prev_hyperlink = self.last_content.last_hovered_hyperlink.take();
let prev_hovered_word = self.last_content.last_hovered_word.take();
let point = grid_point(
*position,
self.last_content.size,
term.grid().display_offset(),
)
.grid_clamp(term, alacritty_terminal::index::Boundary::Cursor);
.grid_clamp(term, alacritty_terminal::index::Boundary::Grid);
let link = term.grid().index(point).hyperlink();
let found_url = if link.is_some() {
let found_word = if link.is_some() {
let mut min_index = point;
loop {
let new_min_index =
@ -847,42 +873,80 @@ impl Terminal {
let url = link.unwrap().uri().to_owned();
let url_match = min_index..=max_index;
Some((url, url_match))
} else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) {
let url = term.bounds_to_string(*url_match.start(), *url_match.end());
Some((url, url_match))
Some((url, true, url_match))
} else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
let maybe_url_or_path =
term.bounds_to_string(*word_match.start(), *word_match.end());
let is_url = match regex_match_at(term, point, &URL_REGEX) {
Some(url_match) => url_match == word_match,
None => false,
};
Some((maybe_url_or_path, is_url, word_match))
} else {
None
};
if let Some((url, url_match)) = found_url {
if *open {
cx.platform().open_url(url.as_str());
} else {
self.update_hyperlink(prev_hyperlink, url, url_match);
match found_word {
Some((maybe_url_or_path, is_url, url_match)) => {
if *open {
let target = if is_url {
MaybeNavigationTarget::Url(maybe_url_or_path)
} else {
MaybeNavigationTarget::PathLike(maybe_url_or_path)
};
cx.emit(Event::Open(target));
} else {
self.update_selected_word(
prev_hovered_word,
url_match,
maybe_url_or_path,
is_url,
cx,
);
}
self.hovered_word = true;
}
None => {
if self.hovered_word {
cx.emit(Event::NewNavigationTarget(None));
}
self.hovered_word = false;
}
}
}
}
}
fn update_hyperlink(
fn update_selected_word(
&mut self,
prev_hyperlink: Option<(String, RangeInclusive<Point>, usize)>,
url: String,
url_match: RangeInclusive<Point>,
prev_word: Option<HoveredWord>,
word_match: RangeInclusive<Point>,
word: String,
is_url: bool,
cx: &mut ModelContext<Self>,
) {
if let Some(prev_hyperlink) = prev_hyperlink {
if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match {
self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2));
} else {
self.last_content.last_hovered_hyperlink =
Some((url, url_match, self.next_link_id()));
if let Some(prev_word) = prev_word {
if prev_word.word == word && prev_word.word_match == word_match {
self.last_content.last_hovered_word = Some(HoveredWord {
word,
word_match,
id: prev_word.id,
});
return;
}
} else {
self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id()));
}
self.last_content.last_hovered_word = Some(HoveredWord {
word: word.clone(),
word_match,
id: self.next_link_id(),
});
let navigation_target = if is_url {
MaybeNavigationTarget::Url(word)
} else {
MaybeNavigationTarget::PathLike(word)
};
cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
}
fn next_link_id(&mut self) -> usize {
@ -964,6 +1028,15 @@ impl Terminal {
}
}
pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
let changed = self.cmd_pressed != modifiers.cmd;
if !self.cmd_pressed && modifiers.cmd {
self.refresh_hovered_word();
}
self.cmd_pressed = modifiers.cmd;
changed
}
///Paste text into the terminal
pub fn paste(&mut self, text: &str) {
let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
@ -1035,7 +1108,7 @@ impl Terminal {
cursor: content.cursor,
cursor_char: term.grid()[content.cursor.point].c,
size: last_content.size,
last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(),
last_hovered_word: last_content.last_hovered_word.clone(),
}
}
@ -1089,14 +1162,14 @@ impl Terminal {
self.pty_tx.notify(bytes);
}
}
} else {
self.hyperlink_from_position(Some(position));
} else if self.cmd_pressed {
self.word_from_position(Some(position));
}
}
fn hyperlink_from_position(&mut self, position: Option<Vector2F>) {
fn word_from_position(&mut self, position: Option<Vector2F>) {
if self.selection_phase == SelectionPhase::Selecting {
self.last_content.last_hovered_hyperlink = None;
self.last_content.last_hovered_word = None;
} else if let Some(position) = position {
self.events
.push_back(InternalEvent::FindHyperlink(position, false));
@ -1208,7 +1281,7 @@ impl Terminal {
let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
cx.platform().open_url(link.uri());
} else {
} else if self.cmd_pressed {
self.events
.push_back(InternalEvent::FindHyperlink(position, true));
}
@ -1255,8 +1328,8 @@ impl Terminal {
}
}
pub fn refresh_hyperlink(&mut self) {
self.hyperlink_from_position(self.last_mouse_position);
fn refresh_hovered_word(&mut self) {
self.word_from_position(self.last_mouse_position);
}
fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option<i32> {
@ -1334,6 +1407,10 @@ impl Terminal {
})
.unwrap_or_else(|| "Terminal".to_string())
}
pub fn can_navigate_to_selected_word(&self) -> bool {
self.cmd_pressed && self.hovered_word
}
}
impl Drop for Terminal {

View File

@ -163,6 +163,7 @@ pub struct TerminalElement {
terminal: WeakModelHandle<Terminal>,
focused: bool,
cursor_visible: bool,
can_navigate_to_selected_word: bool,
}
impl TerminalElement {
@ -170,11 +171,13 @@ impl TerminalElement {
terminal: WeakModelHandle<Terminal>,
focused: bool,
cursor_visible: bool,
can_navigate_to_selected_word: bool,
) -> TerminalElement {
TerminalElement {
terminal,
focused,
cursor_visible,
can_navigate_to_selected_word,
}
}
@ -580,20 +583,30 @@ impl Element<TerminalView> for TerminalElement {
let background_color = terminal_theme.background;
let terminal_handle = self.terminal.upgrade(cx).unwrap();
let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| {
let last_hovered_word = terminal_handle.update(cx, |terminal, cx| {
terminal.set_size(dimensions);
terminal.try_sync(cx);
terminal.last_content.last_hovered_hyperlink.clone()
if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() {
terminal.last_content.last_hovered_word.clone()
} else {
None
}
});
let hyperlink_tooltip = last_hovered_hyperlink.map(|(uri, _, id)| {
let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
let mut tooltip = Overlay::new(
Empty::new()
.contained()
.constrained()
.with_width(dimensions.width())
.with_height(dimensions.height())
.with_tooltip::<TerminalElement>(id, uri, None, tooltip_style, cx),
.with_tooltip::<TerminalElement>(
hovered_word.id,
hovered_word.word,
None,
tooltip_style,
cx,
),
)
.with_position_mode(gpui::elements::OverlayPositionMode::Local)
.into_any();
@ -613,7 +626,6 @@ impl Element<TerminalView> for TerminalElement {
cursor_char,
selection,
cursor,
last_hovered_hyperlink,
..
} = { &terminal_handle.read(cx).last_content };
@ -634,9 +646,9 @@ impl Element<TerminalView> for TerminalElement {
&terminal_theme,
cx.text_layout_cache(),
cx.font_cache(),
last_hovered_hyperlink
last_hovered_word
.as_ref()
.map(|(_, range, _)| (link_style, range)),
.map(|last_hovered_word| (link_style, &last_hovered_word.word_match)),
);
//Layout cursor. Rectangle is used for IME, so we should lay it out even

View File

@ -261,10 +261,14 @@ impl TerminalPanel {
.create_terminal(working_directory, window_id, cx)
.log_err()
}) {
let terminal =
Box::new(cx.add_view(|cx| {
TerminalView::new(terminal, workspace.database_id(), cx)
}));
let terminal = Box::new(cx.add_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
}));
pane.update(cx, |pane, cx| {
let focus = pane.has_focus();
pane.add_item(terminal, true, focus, None, cx);

View File

@ -3,18 +3,21 @@ pub mod terminal_element;
pub mod terminal_panel;
use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
use anyhow::Context;
use context_menu::{ContextMenu, ContextMenuItem};
use dirs::home_dir;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
actions,
elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack},
geometry::vector::Vector2F,
impl_actions,
keymap_matcher::{KeymapContext, Keystroke},
platform::KeyDownEvent,
platform::{KeyDownEvent, ModifiersChangedEvent},
AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext,
ViewHandle, WeakViewHandle,
};
use language::Bias;
use project::{LocalWorktree, Project};
use serde::Deserialize;
use smallvec::{smallvec, SmallVec};
@ -30,9 +33,9 @@ use terminal::{
index::Point,
term::{search::RegexSearch, TermMode},
},
Event, Terminal, TerminalBlink, WorkingDirectory,
Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory,
};
use util::ResultExt;
use util::{paths::PathLikeWithPosition, ResultExt};
use workspace::{
item::{BreadcrumbText, Item, ItemEvent},
notifications::NotifyResultExt,
@ -90,6 +93,7 @@ pub struct TerminalView {
blinking_on: bool,
blinking_paused: bool,
blink_epoch: usize,
can_navigate_to_selected_word: bool,
workspace_id: WorkspaceId,
}
@ -117,19 +121,27 @@ impl TerminalView {
.notify_err(workspace, cx);
if let Some(terminal) = terminal {
let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
let view = cx.add_view(|cx| {
TerminalView::new(
terminal,
workspace.weak_handle(),
workspace.database_id(),
cx,
)
});
workspace.add_item(Box::new(view), cx)
}
}
pub fn new(
terminal: ModelHandle<Terminal>,
workspace: WeakViewHandle<Workspace>,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Self>,
) -> Self {
let view_id = cx.view_id();
cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
cx.subscribe(&terminal, |this, _, event, cx| match event {
cx.subscribe(&terminal, move |this, _, event, cx| match event {
Event::Wakeup => {
if !cx.is_self_focused() {
this.has_new_content = true;
@ -158,7 +170,82 @@ impl TerminalView {
.detach();
}
}
_ => cx.emit(*event),
Event::NewNavigationTarget(maybe_navigation_target) => {
this.can_navigate_to_selected_word = match maybe_navigation_target {
Some(MaybeNavigationTarget::Url(_)) => true,
Some(MaybeNavigationTarget::PathLike(maybe_path)) => {
!possible_open_targets(&workspace, maybe_path, cx).is_empty()
}
None => false,
}
}
Event::Open(maybe_navigation_target) => match maybe_navigation_target {
MaybeNavigationTarget::Url(url) => cx.platform().open_url(url),
MaybeNavigationTarget::PathLike(maybe_path) => {
if !this.can_navigate_to_selected_word {
return;
}
let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx);
if let Some(path) = potential_abs_paths.into_iter().next() {
let is_dir = path.path_like.is_dir();
let task_workspace = workspace.clone();
cx.spawn(|_, mut cx| async move {
let opened_items = task_workspace
.update(&mut cx, |workspace, cx| {
workspace.open_paths(vec![path.path_like], is_dir, cx)
})
.context("workspace update")?
.await;
anyhow::ensure!(
opened_items.len() == 1,
"For a single path open, expected single opened item"
);
let opened_item = opened_items
.into_iter()
.next()
.unwrap()
.transpose()
.context("path open")?;
if is_dir {
task_workspace.update(&mut cx, |workspace, cx| {
workspace.project().update(cx, |_, cx| {
cx.emit(project::Event::ActivateProjectPanel);
})
})?;
} else {
if let Some(row) = path.row {
let col = path.column.unwrap_or(0);
if let Some(active_editor) =
opened_item.and_then(|item| item.downcast::<Editor>())
{
active_editor
.downgrade()
.update(&mut cx, |editor, cx| {
let snapshot = editor.snapshot(cx).display_snapshot;
let point = snapshot.buffer_snapshot.clip_point(
language::Point::new(
row.saturating_sub(1),
col.saturating_sub(1),
),
Bias::Left,
);
editor.change_selections(
Some(Autoscroll::center()),
cx,
|s| s.select_ranges([point..point]),
);
})
.log_err();
}
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
},
_ => cx.emit(event.clone()),
})
.detach();
@ -171,6 +258,7 @@ impl TerminalView {
blinking_on: false,
blinking_paused: false,
blink_epoch: 0,
can_navigate_to_selected_word: false,
workspace_id,
}
}
@ -344,6 +432,50 @@ impl TerminalView {
}
}
fn possible_open_targets(
workspace: &WeakViewHandle<Workspace>,
maybe_path: &String,
cx: &mut ViewContext<'_, '_, TerminalView>,
) -> Vec<PathLikeWithPosition<PathBuf>> {
let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| {
Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf())
})
.expect("infallible");
let maybe_path = path_like.path_like;
let potential_abs_paths = if maybe_path.is_absolute() {
vec![maybe_path]
} else if maybe_path.starts_with("~") {
if let Some(abs_path) = maybe_path
.strip_prefix("~")
.ok()
.and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path)))
{
vec![abs_path]
} else {
Vec::new()
}
} else if let Some(workspace) = workspace.upgrade(cx) {
workspace.update(cx, |workspace, cx| {
workspace
.worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
.collect()
})
} else {
Vec::new()
};
potential_abs_paths
.into_iter()
.filter(|path| path.exists())
.map(|path| PathLikeWithPosition {
path_like: path,
row: path_like.row,
column: path_like.column,
})
.collect()
}
pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
let searcher = match query {
project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
@ -372,6 +504,7 @@ impl View for TerminalView {
terminal_handle,
focused,
self.should_show_cursor(focused, cx),
self.can_navigate_to_selected_word,
)
.contained(),
)
@ -393,6 +526,20 @@ impl View for TerminalView {
cx.notify();
}
fn modifiers_changed(
&mut self,
event: &ModifiersChangedEvent,
cx: &mut ViewContext<Self>,
) -> bool {
let handled = self
.terminal()
.update(cx, |term, _| term.try_modifiers_change(&event.modifiers));
if handled {
cx.notify();
}
handled
}
fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
self.clear_bel(cx);
self.pause_cursor_blinking(cx);
@ -618,7 +765,7 @@ impl Item for TerminalView {
project.create_terminal(cwd, window_id, cx)
})?;
Ok(pane.update(&mut cx, |_, cx| {
cx.add_view(|cx| TerminalView::new(terminal, workspace_id, cx))
cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx))
})?)
})
}

View File

@ -402,6 +402,7 @@ pub struct StatusBar {
pub height: f32,
pub item_spacing: f32,
pub cursor_position: TextStyle,
pub vim_mode_indicator: ContainedText,
pub active_language: Interactive<ContainedText>,
pub auto_update_progress_message: TextStyle,
pub auto_update_done_message: TextStyle,
@ -480,8 +481,10 @@ pub struct ProjectPanelEntry {
#[serde(flatten)]
pub container: ContainerStyle,
pub text: TextStyle,
pub icon_color: Color,
pub icon_size: f32,
pub icon_color: Color,
pub chevron_color: Color,
pub chevron_size: f32,
pub icon_spacing: f32,
pub status: EntryStatus,
}
@ -689,6 +692,8 @@ pub struct Editor {
pub document_highlight_read_background: Color,
pub document_highlight_write_background: Color,
pub diff: DiffStyle,
pub wrap_guide: Color,
pub active_wrap_guide: Color,
pub line_number: Color,
pub line_number_active: Color,
pub guest_selections: Vec<SelectionStyle>,

View File

@ -32,6 +32,8 @@ language = { path = "../language" }
search = { path = "../search" }
settings = { path = "../settings" }
workspace = { path = "../workspace" }
theme = { path = "../theme" }
language_selector = { path = "../language_selector"}
[dev-dependencies]
indoc.workspace = true
@ -44,3 +46,4 @@ project = { path = "../project", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
settings = { path = "../settings" }
workspace = { path = "../workspace", features = ["test-support"] }
theme = { path = "../theme", features = ["test-support"] }

View File

@ -13,7 +13,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
cx.update_window(previously_active_editor.window_id(), |cx| {
Vim::update(cx, |vim, cx| {
vim.update_active_editor(cx, |previously_active_editor, cx| {
Vim::unhook_vim_settings(previously_active_editor, cx);
vim.unhook_vim_settings(previously_active_editor, cx)
});
});
});
@ -35,7 +35,7 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
}
}
editor.update(cx, |editor, cx| Vim::unhook_vim_settings(editor, cx))
editor.update(cx, |editor, cx| vim.unhook_vim_settings(editor, cx))
});
});
}

View File

@ -0,0 +1,58 @@
use gpui::{elements::Label, AnyElement, Element, Entity, View, ViewContext};
use workspace::{item::ItemHandle, StatusItemView};
use crate::state::Mode;
pub struct ModeIndicator {
pub mode: Mode,
}
impl ModeIndicator {
pub fn new(mode: Mode) -> Self {
Self { mode }
}
pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
if mode != self.mode {
self.mode = mode;
cx.notify();
}
}
}
impl Entity for ModeIndicator {
type Event = ();
}
impl View for ModeIndicator {
fn ui_name() -> &'static str {
"ModeIndicatorView"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
let theme = &theme::current(cx).workspace.status_bar;
// we always choose text to be 12 monospace characters
// so that as the mode indicator changes, the rest of the
// UI stays still.
let text = match self.mode {
Mode::Normal => "-- NORMAL --",
Mode::Insert => "-- INSERT --",
Mode::Visual { line: false } => "-- VISUAL --",
Mode::Visual { line: true } => "VISUAL LINE ",
};
Label::new(text, theme.vim_mode_indicator.text.clone())
.contained()
.with_style(theme.vim_mode_indicator.container)
.into_any()
}
}
impl StatusItemView for ModeIndicator {
fn set_active_pane_item(
&mut self,
_active_pane_item: Option<&dyn ItemHandle>,
_cx: &mut ViewContext<Self>,
) {
// nothing to do.
}
}

View File

@ -62,6 +62,12 @@ struct PreviousWordStart {
ignore_punctuation: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
struct RepeatFind {
#[serde(default)]
backwards: bool,
}
actions!(
vim,
[
@ -82,7 +88,10 @@ actions!(
NextLineStart,
]
);
impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
impl_actions!(
vim,
[NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
@ -123,13 +132,15 @@ pub fn init(cx: &mut AppContext) {
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx))
cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
repeat_motion(action.backwards, cx)
})
}
pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
if let Some(Operator::Namespace(_))
| Some(Operator::FindForward { .. })
| Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
Vim::read(cx).active_operator()
{
Vim::update(cx, |vim, cx| vim.pop_operator(cx));
}
@ -146,6 +157,35 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| vim.clear_operator(cx));
}
fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
let find = match Vim::read(cx).state.last_find.clone() {
Some(Motion::FindForward { before, text }) => {
if backwards {
Motion::FindBackward {
after: before,
text,
}
} else {
Motion::FindForward { before, text }
}
}
Some(Motion::FindBackward { after, text }) => {
if backwards {
Motion::FindForward {
before: after,
text,
}
} else {
Motion::FindBackward { after, text }
}
}
_ => return,
};
motion(find, cx)
}
// Motion handling is specified here:
// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
impl Motion {
@ -743,4 +783,23 @@ mod test {
cx.simulate_shared_keystrokes(["%"]).await;
cx.assert_shared_state("func boop(ˇ) {\n}").await;
}
#[gpui::test]
async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;
cx.set_shared_state("ˇone two three four").await;
cx.simulate_shared_keystrokes(["f", "o"]).await;
cx.assert_shared_state("one twˇo three four").await;
cx.simulate_shared_keystrokes([","]).await;
cx.assert_shared_state("ˇone two three four").await;
cx.simulate_shared_keystrokes(["2", ";"]).await;
cx.assert_shared_state("one two three fˇour").await;
cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
cx.assert_shared_state("one two threeˇ four").await;
cx.simulate_shared_keystrokes(["3", ";"]).await;
cx.assert_shared_state("oneˇ two three four").await;
cx.simulate_shared_keystrokes([","]).await;
cx.assert_shared_state("one two thˇree four").await;
}
}

View File

@ -2,6 +2,7 @@ mod case;
mod change;
mod delete;
mod scroll;
mod search;
mod substitute;
mod yank;
@ -57,6 +58,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(insert_line_above);
cx.add_action(insert_line_below);
cx.add_action(change_case);
search::init(cx);
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
Vim::update(cx, |vim, cx| {
let times = vim.pop_number_operator(cx);
@ -105,7 +107,7 @@ pub fn normal_motion(
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
Some(operator) => {
// Can't do anything for text objects or namespace operators. Ignoring
// Can't do anything for text objects, Ignoring
error!("Unexpected normal mode motion operator: {:?}", operator)
}
}
@ -439,11 +441,8 @@ mod test {
use indoc::indoc;
use crate::{
state::{
Mode::{self, *},
Namespace, Operator,
},
test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
state::Mode::{self},
test::{ExemptionFeatures, NeovimBackedTestContext},
};
#[gpui::test]
@ -608,22 +607,6 @@ mod test {
.await;
}
#[gpui::test]
async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// Can abort with escape to get back to normal mode
cx.simulate_keystroke("g");
assert_eq!(cx.mode(), Normal);
assert_eq!(
cx.active_operator(),
Some(Operator::Namespace(Namespace::G))
);
cx.simulate_keystroke("escape");
assert_eq!(cx.mode(), Normal);
assert_eq!(cx.active_operator(), None);
}
#[gpui::test]
async fn test_gg(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;

View File

@ -0,0 +1,302 @@
use gpui::{actions, impl_actions, AppContext, ViewContext};
use search::{buffer_search, BufferSearchBar, SearchOptions};
use serde_derive::Deserialize;
use workspace::{searchable::Direction, Pane, Workspace};
use crate::{state::SearchState, Vim};
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MoveToNext {
#[serde(default)]
partial_word: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MoveToPrev {
#[serde(default)]
partial_word: bool,
}
#[derive(Clone, Deserialize, PartialEq)]
pub(crate) struct Search {
#[serde(default)]
backwards: bool,
}
impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
actions!(vim, [SearchSubmit]);
pub(crate) fn init(cx: &mut AppContext) {
cx.add_action(move_to_next);
cx.add_action(move_to_prev);
cx.add_action(search);
cx.add_action(search_submit);
cx.add_action(search_deploy);
}
fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
}
fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
}
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
let pane = workspace.active_pane().clone();
let direction = if action.backwards {
Direction::Prev
} else {
Direction::Next
};
Vim::update(cx, |vim, cx| {
let count = vim.pop_number_operator(cx).unwrap_or(1);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
if !search_bar.show(cx) {
return;
}
let query = search_bar.query(cx);
search_bar.select_query(cx);
cx.focus_self();
if query.is_empty() {
search_bar.set_search_options(
SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX,
cx,
);
}
vim.state.search = SearchState {
direction,
count,
initial_query: query,
};
});
}
})
})
}
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
Vim::update(cx, |vim, _| vim.state.search = Default::default());
cx.propagate_action();
}
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
let mut state = &mut vim.state.search;
let mut count = state.count;
// in the case that the query has changed, the search bar
// will have selected the next match already.
if (search_bar.query(cx) != state.initial_query)
&& state.direction == Direction::Next
{
count = count.saturating_sub(1)
}
search_bar.select_match(state.direction, count, cx);
state.count = 1;
search_bar.focus_editor(&Default::default(), cx);
});
}
});
})
}
pub fn move_to_internal(
workspace: &mut Workspace,
direction: Direction,
whole_word: bool,
cx: &mut ViewContext<Workspace>,
) {
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
let count = vim.pop_number_operator(cx).unwrap_or(1);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let search = search_bar.update(cx, |search_bar, cx| {
let mut options = SearchOptions::CASE_SENSITIVE;
options.set(SearchOptions::WHOLE_WORD, whole_word);
if search_bar.show(cx) {
search_bar
.query_suggestion(cx)
.map(|query| search_bar.search(&query, Some(options), cx))
} else {
None
}
});
if let Some(search) = search {
let search_bar = search_bar.downgrade();
cx.spawn(|_, mut cx| async move {
search.await?;
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(direction, count, cx)
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
});
vim.clear_operator(cx);
});
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use editor::DisplayPoint;
use search::BufferSearchBar;
use crate::{state::Mode, test::VimTestContext};
#[gpui::test]
async fn test_move_to_next(
cx: &mut gpui::TestAppContext,
deterministic: Arc<gpui::executor::Deterministic>,
) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
cx.simulate_keystrokes(["*"]);
deterministic.run_until_parked();
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
cx.simulate_keystrokes(["*"]);
deterministic.run_until_parked();
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
cx.simulate_keystrokes(["#"]);
deterministic.run_until_parked();
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
cx.simulate_keystrokes(["#"]);
deterministic.run_until_parked();
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
cx.simulate_keystrokes(["2", "*"]);
deterministic.run_until_parked();
cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
cx.simulate_keystrokes(["g", "*"]);
deterministic.run_until_parked();
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
cx.simulate_keystrokes(["n"]);
cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
cx.simulate_keystrokes(["g", "#"]);
deterministic.run_until_parked();
cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
}
#[gpui::test]
async fn test_search(
cx: &mut gpui::TestAppContext,
deterministic: Arc<gpui::executor::Deterministic>,
) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
cx.simulate_keystrokes(["/", "c", "c"]);
let search_bar = cx.workspace(|workspace, cx| {
workspace
.active_pane()
.read(cx)
.toolbar()
.read(cx)
.item_of_type::<BufferSearchBar>()
.expect("Buffer search bar should be deployed")
});
search_bar.read_with(cx.cx, |bar, cx| {
assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
});
deterministic.run_until_parked();
cx.update_editor(|editor, cx| {
let highlights = editor.all_background_highlights(cx);
assert_eq!(3, highlights.len());
assert_eq!(
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
highlights[0].0
)
});
cx.simulate_keystrokes(["enter"]);
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
// n to go to next/N to go to previous
cx.simulate_keystrokes(["n"]);
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
cx.simulate_keystrokes(["shift-n"]);
cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
// ?<enter> to go to previous
cx.simulate_keystrokes(["?", "enter"]);
deterministic.run_until_parked();
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
cx.simulate_keystrokes(["?", "enter"]);
deterministic.run_until_parked();
cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
// /<enter> to go to next
cx.simulate_keystrokes(["/", "enter"]);
deterministic.run_until_parked();
cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
// ?{search}<enter> to search backwards
cx.simulate_keystrokes(["?", "b", "enter"]);
deterministic.run_until_parked();
cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
// works with counts
cx.simulate_keystrokes(["4", "/", "c"]);
deterministic.run_until_parked();
cx.simulate_keystrokes(["enter"]);
cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
// check that searching resumes from cursor, not previous match
cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
cx.simulate_keystrokes(["/", "d"]);
deterministic.run_until_parked();
cx.simulate_keystrokes(["enter"]);
cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
cx.simulate_keystrokes(["/", "b"]);
deterministic.run_until_parked();
cx.simulate_keystrokes(["enter"]);
cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
}
#[gpui::test]
async fn test_non_vim_search(
cx: &mut gpui::TestAppContext,
deterministic: Arc<gpui::executor::Deterministic>,
) {
let mut cx = VimTestContext::new(cx, false).await;
cx.set_state("ˇone one one one", Mode::Normal);
cx.simulate_keystrokes(["cmd-f"]);
deterministic.run_until_parked();
cx.assert_editor_state("«oneˇ» one one one");
cx.simulate_keystrokes(["enter"]);
cx.assert_editor_state("one «oneˇ» one one");
cx.simulate_keystrokes(["shift-enter"]);
cx.assert_editor_state("«oneˇ» one one one");
}
}

View File

@ -1,6 +1,9 @@
use gpui::keymap_matcher::KeymapContext;
use language::CursorShape;
use serde::{Deserialize, Serialize};
use workspace::searchable::Direction;
use crate::motion::Motion;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub enum Mode {
@ -15,16 +18,9 @@ impl Default for Mode {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Namespace {
G,
Z,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
pub enum Operator {
Number(usize),
Namespace(Namespace),
Change,
Delete,
Yank,
@ -38,6 +34,25 @@ pub enum Operator {
pub struct VimState {
pub mode: Mode,
pub operator_stack: Vec<Operator>,
pub search: SearchState,
pub last_find: Option<Motion>,
}
pub struct SearchState {
pub direction: Direction,
pub count: usize,
pub initial_query: String,
}
impl Default for SearchState {
fn default() -> Self {
Self {
direction: Direction::Next,
count: 1,
initial_query: "".to_string(),
}
}
}
impl VimState {
@ -73,6 +88,7 @@ impl VimState {
pub fn keymap_context_layer(&self) -> KeymapContext {
let mut context = KeymapContext::default();
context.add_identifier("VimEnabled");
context.add_key(
"vim_mode",
match self.mode {
@ -107,8 +123,6 @@ impl Operator {
pub fn id(&self) -> &'static str {
match self {
Operator::Number(_) => "n",
Operator::Namespace(Namespace::G) => "g",
Operator::Namespace(Namespace::Z) => "z",
Operator::Object { around: false } => "i",
Operator::Object { around: true } => "a",
Operator::Change => "c",

View File

@ -4,7 +4,10 @@ mod neovim_connection;
mod vim_binding_test_context;
mod vim_test_context;
use std::sync::Arc;
use command_palette::CommandPalette;
use editor::DisplayPoint;
pub use neovim_backed_binding_test_context::*;
pub use neovim_backed_test_context::*;
pub use vim_binding_test_context::*;
@ -13,7 +16,7 @@ pub use vim_test_context::*;
use indoc::indoc;
use search::BufferSearchBar;
use crate::state::Mode;
use crate::{state::Mode, ModeIndicator};
#[gpui::test]
async fn test_initially_disabled(cx: &mut gpui::TestAppContext) {
@ -96,7 +99,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) {
});
search_bar.read_with(cx.cx, |bar, cx| {
assert_eq!(bar.query_editor.read(cx).text(cx), "jumps");
assert_eq!(bar.query_editor.read(cx).text(cx), "");
})
}
@ -137,7 +140,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
cx.assert_editor_state("aa\nbˇb\ncc");
// works in visuial mode
cx.simulate_keystrokes(["shift-v", "down", ">", ">"]);
cx.simulate_keystrokes(["shift-v", "down", ">"]);
cx.assert_editor_state("aa\n b«b\n cˇ»c");
}
@ -153,3 +156,98 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) {
assert!(!cx.workspace(|workspace, _| workspace.modal::<CommandPalette>().is_some()));
cx.assert_state("aˇbc\n", Mode::Insert);
}
#[gpui::test]
async fn test_selection_on_search(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal);
cx.simulate_keystrokes(["/", "c", "c"]);
let search_bar = cx.workspace(|workspace, cx| {
workspace
.active_pane()
.read(cx)
.toolbar()
.read(cx)
.item_of_type::<BufferSearchBar>()
.expect("Buffer search bar should be deployed")
});
search_bar.read_with(cx.cx, |bar, cx| {
assert_eq!(bar.query_editor.read(cx).text(cx), "cc");
});
// wait for the query editor change event to fire.
search_bar.next_notification(&cx).await;
cx.update_editor(|editor, cx| {
let highlights = editor.all_background_highlights(cx);
assert_eq!(3, highlights.len());
assert_eq!(
DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
highlights[0].0
)
});
cx.simulate_keystrokes(["enter"]);
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
cx.simulate_keystrokes(["n"]);
cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal);
cx.simulate_keystrokes(["shift-n"]);
cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal);
}
#[gpui::test]
async fn test_status_indicator(
cx: &mut gpui::TestAppContext,
deterministic: Arc<gpui::executor::Deterministic>,
) {
let mut cx = VimTestContext::new(cx, true).await;
deterministic.run_until_parked();
let mode_indicator = cx.workspace(|workspace, cx| {
let status_bar = workspace.status_bar().read(cx);
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
assert!(mode_indicator.is_some());
mode_indicator.unwrap()
});
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Mode::Normal
);
// shows the correct mode
cx.simulate_keystrokes(["i"]);
deterministic.run_until_parked();
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Mode::Insert
);
// shows even in search
cx.simulate_keystrokes(["escape", "v", "/"]);
deterministic.run_until_parked();
assert_eq!(
cx.workspace(|_, cx| mode_indicator.read(cx).mode),
Mode::Visual { line: false }
);
// hides if vim mode is disabled
cx.disable_vim();
deterministic.run_until_parked();
cx.workspace(|workspace, cx| {
let status_bar = workspace.status_bar().read(cx);
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
assert!(mode_indicator.is_none());
});
cx.enable_vim();
deterministic.run_until_parked();
cx.workspace(|workspace, cx| {
let status_bar = workspace.status_bar().read(cx);
let mode_indicator = status_bar.item_of_type::<ModeIndicator>();
assert!(mode_indicator.is_some());
});
}

View File

@ -90,6 +90,7 @@ impl<'a> VimTestContext<'a> {
self.cx.set_state(text)
}
#[track_caller]
pub fn assert_state(&mut self, text: &str, mode: Mode) {
self.assert_editor_state(text);
assert_eq!(self.mode(), mode, "{}", self.assertion_context());

View File

@ -3,6 +3,7 @@ mod test;
mod editor_events;
mod insert;
mod mode_indicator;
mod motion;
mod normal;
mod object;
@ -14,10 +15,11 @@ use anyhow::Result;
use collections::CommandPaletteFilter;
use editor::{Bias, Editor, EditorMode, Event};
use gpui::{
actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle,
WindowContext,
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
};
use language::CursorShape;
pub use mode_indicator::ModeIndicator;
use motion::Motion;
use normal::normal_replace;
use serde::Deserialize;
@ -90,7 +92,10 @@ pub fn init(cx: &mut AppContext) {
}
pub fn observe_keystrokes(cx: &mut WindowContext) {
cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| {
cx.observe_keystrokes(|_keystroke, result, handled_by, cx| {
if result == &MatchResult::Pending {
return true;
}
if let Some(handled_by) = handled_by {
// Keystroke is handled by the vim system, so continue forward
if handled_by.namespace() == "vim" {
@ -116,6 +121,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
pub struct Vim {
active_editor: Option<WeakViewHandle<Editor>>,
editor_subscription: Option<Subscription>,
mode_indicator: Option<ViewHandle<ModeIndicator>>,
enabled: bool,
state: VimState,
@ -175,6 +181,10 @@ impl Vim {
self.state.mode = mode;
self.state.operator_stack.clear();
if let Some(mode_indicator) = &self.mode_indicator {
mode_indicator.update(cx, |mode_indicator, cx| mode_indicator.set_mode(mode, cx))
}
// Sync editor settings like clip mode
self.sync_vim_settings(cx);
@ -243,10 +253,14 @@ impl Vim {
match Vim::read(cx).active_operator() {
Some(Operator::FindForward { before }) => {
motion::motion(Motion::FindForward { before, text }, cx)
let find = Motion::FindForward { before, text };
Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
motion::motion(find, cx)
}
Some(Operator::FindBackward { after }) => {
motion::motion(Motion::FindBackward { after, text }, cx)
let find = Motion::FindBackward { after, text };
Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone()));
motion::motion(find, cx)
}
Some(Operator::Replace) => match Vim::read(cx).state.mode {
Mode::Normal => normal_replace(text, cx),
@ -257,6 +271,44 @@ impl Vim {
}
}
fn sync_mode_indicator(cx: &mut WindowContext) {
let Some(workspace) = cx.root_view()
.downcast_ref::<Workspace>()
.map(|workspace| workspace.downgrade()) else {
return;
};
cx.spawn(|mut cx| async move {
workspace.update(&mut cx, |workspace, cx| {
Vim::update(cx, |vim, cx| {
workspace.status_bar().update(cx, |status_bar, cx| {
let current_position = status_bar.position_of_item::<ModeIndicator>();
if vim.enabled && current_position.is_none() {
if vim.mode_indicator.is_none() {
vim.mode_indicator =
Some(cx.add_view(|_| ModeIndicator::new(vim.state.mode)));
};
let mode_indicator = vim.mode_indicator.as_ref().unwrap();
let position = status_bar
.position_of_item::<language_selector::ActiveBufferLanguage>();
if let Some(position) = position {
status_bar.insert_item_after(position, mode_indicator.clone(), cx)
} else {
status_bar.add_left_item(mode_indicator.clone(), cx)
}
} else if !vim.enabled {
if let Some(position) = current_position {
status_bar.remove_item_at(position, cx)
}
}
})
})
})
})
.detach_and_log_err(cx);
}
fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) {
if self.enabled != enabled {
self.enabled = enabled;
@ -295,22 +347,39 @@ impl Vim {
if self.enabled && editor.mode() == EditorMode::Full {
editor.set_cursor_shape(cursor_shape, cx);
editor.set_clip_at_line_ends(state.clip_at_line_end(), cx);
editor.set_collapse_matches(true);
editor.set_input_enabled(!state.vim_controlled());
editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true });
let context_layer = state.keymap_context_layer();
editor.set_keymap_context_layer::<Self>(context_layer, cx);
} else {
Self::unhook_vim_settings(editor, cx);
// Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur,
// but we need collapse_matches to persist when the search bar is focused.
editor.set_collapse_matches(false);
self.unhook_vim_settings(editor, cx);
}
});
Vim::sync_mode_indicator(cx);
}
fn unhook_vim_settings(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext<Editor>) {
editor.set_cursor_shape(CursorShape::Bar, cx);
editor.set_clip_at_line_ends(false, cx);
editor.set_input_enabled(true);
editor.selections.line_mode = false;
editor.remove_keymap_context_layer::<Self>(cx);
// we set the VimEnabled context on all editors so that we
// can distinguish between vim mode and non-vim mode in the BufferSearchBar.
// This is a bit of a hack, but currently the search crate does not depend on vim,
// and it seems nice to keep it that way.
if self.enabled {
let mut context = KeymapContext::default();
context.add_identifier("VimEnabled");
editor.set_keymap_context_layer::<Self>(context, cx)
} else {
editor.remove_keymap_context_layer::<Self>(cx);
}
}
}

View File

@ -58,7 +58,9 @@ pub fn visual_motion(motion: Motion, times: Option<usize>, cx: &mut WindowContex
pub fn visual_object(object: Object, cx: &mut WindowContext) {
Vim::update(cx, |vim, cx| {
if let Operator::Object { around } = vim.pop_operator(cx) {
if let Some(Operator::Object { around }) = vim.active_operator() {
vim.pop_operator(cx);
vim.update_active_editor(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {

View File

@ -0,0 +1,17 @@
{"Put":{"state":"ˇone two three four"}}
{"Key":"f"}
{"Key":"o"}
{"Get":{"state":"one twˇo three four","mode":"Normal"}}
{"Key":","}
{"Get":{"state":"ˇone two three four","mode":"Normal"}}
{"Key":"2"}
{"Key":";"}
{"Get":{"state":"one two three fˇour","mode":"Normal"}}
{"Key":"shift-t"}
{"Key":"e"}
{"Get":{"state":"one two threeˇ four","mode":"Normal"}}
{"Key":"3"}
{"Key":";"}
{"Get":{"state":"oneˇ two three four","mode":"Normal"}}
{"Key":","}
{"Get":{"state":"one two thˇree four","mode":"Normal"}}

View File

@ -5,6 +5,7 @@ use crate::{
use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings};
use anyhow::Result;
use client::{proto, Client};
use gpui::geometry::vector::Vector2F;
use gpui::{
fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View,
ViewContext, ViewHandle, WeakViewHandle, WindowContext,
@ -203,6 +204,9 @@ pub trait Item: View {
fn show_toolbar(&self) -> bool {
true
}
fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
None
}
}
pub trait ItemHandle: 'static + fmt::Debug {
@ -271,6 +275,7 @@ pub trait ItemHandle: 'static + fmt::Debug {
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
fn serialized_item_kind(&self) -> Option<&'static str>;
fn show_toolbar(&self, cx: &AppContext) -> bool;
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F>;
}
pub trait WeakItemHandle {
@ -615,6 +620,10 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
fn show_toolbar(&self, cx: &AppContext) -> bool {
self.read(cx).show_toolbar()
}
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
self.read(cx).pixel_position_of_cursor()
}
}
impl From<Box<dyn ItemHandle>> for AnyViewHandle {

View File

@ -542,6 +542,12 @@ impl Pane {
self.items.get(self.active_item_index).cloned()
}
pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
self.items
.get(self.active_item_index)?
.pixel_position_of_cursor(cx)
}
pub fn item_for_entry(
&self,
entry_id: ProjectEntryId,

View File

@ -54,6 +54,20 @@ impl PaneGroup {
}
}
pub fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
match &self.root {
Member::Pane(_) => None,
Member::Axis(axis) => axis.bounding_box_for_pane(pane),
}
}
pub fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
match &self.root {
Member::Pane(pane) => Some(pane),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
}
}
/// Returns:
/// - Ok(true) if it found and removed a pane
/// - Ok(false) if it found but did not remove the pane
@ -309,15 +323,18 @@ pub(crate) struct PaneAxis {
pub axis: Axis,
pub members: Vec<Member>,
pub flexes: Rc<RefCell<Vec<f32>>>,
pub bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
}
impl PaneAxis {
pub fn new(axis: Axis, members: Vec<Member>) -> Self {
let flexes = Rc::new(RefCell::new(vec![1.; members.len()]));
let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
Self {
axis,
members,
flexes,
bounding_boxes,
}
}
@ -326,10 +343,12 @@ impl PaneAxis {
debug_assert!(members.len() == flexes.len());
let flexes = Rc::new(RefCell::new(flexes));
let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()]));
Self {
axis,
members,
flexes,
bounding_boxes,
}
}
@ -409,6 +428,44 @@ impl PaneAxis {
}
}
fn bounding_box_for_pane(&self, pane: &ViewHandle<Pane>) -> Option<RectF> {
debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
for (idx, member) in self.members.iter().enumerate() {
match member {
Member::Pane(found) => {
if pane == found {
return self.bounding_boxes.borrow()[idx];
}
}
Member::Axis(axis) => {
if let Some(rect) = axis.bounding_box_for_pane(pane) {
return Some(rect);
}
}
}
}
None
}
fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle<Pane>> {
debug_assert!(self.members.len() == self.bounding_boxes.borrow().len());
let bounding_boxes = self.bounding_boxes.borrow();
for (idx, member) in self.members.iter().enumerate() {
if let Some(coordinates) = bounding_boxes[idx] {
if coordinates.contains_point(coordinate) {
return match member {
Member::Pane(found) => Some(found),
Member::Axis(axis) => axis.pane_at_pixel_position(coordinate),
};
}
}
}
None
}
fn render(
&self,
project: &ModelHandle<Project>,
@ -423,7 +480,12 @@ impl PaneAxis {
) -> AnyElement<Workspace> {
debug_assert!(self.members.len() == self.flexes.borrow().len());
let mut pane_axis = PaneAxisElement::new(self.axis, basis, self.flexes.clone());
let mut pane_axis = PaneAxisElement::new(
self.axis,
basis,
self.flexes.clone(),
self.bounding_boxes.clone(),
);
let mut active_pane_ix = None;
let mut members = self.members.iter().enumerate().peekable();
@ -546,14 +608,21 @@ mod element {
active_pane_ix: Option<usize>,
flexes: Rc<RefCell<Vec<f32>>>,
children: Vec<AnyElement<Workspace>>,
bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
}
impl PaneAxisElement {
pub fn new(axis: Axis, basis: usize, flexes: Rc<RefCell<Vec<f32>>>) -> Self {
pub fn new(
axis: Axis,
basis: usize,
flexes: Rc<RefCell<Vec<f32>>>,
bounding_boxes: Rc<RefCell<Vec<Option<RectF>>>>,
) -> Self {
Self {
axis,
basis,
flexes,
bounding_boxes,
active_pane_ix: None,
children: Default::default(),
}
@ -708,11 +777,16 @@ mod element {
let mut child_origin = bounds.origin();
let mut bounding_boxes = self.bounding_boxes.borrow_mut();
bounding_boxes.clear();
let mut children_iter = self.children.iter_mut().enumerate().peekable();
while let Some((ix, child)) = children_iter.next() {
let child_start = child_origin.clone();
child.paint(scene, child_origin, visible_bounds, view, cx);
bounding_boxes.push(Some(RectF::new(child_origin, child.size())));
match self.axis {
Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0),
Axis::Vertical => child_origin += vec2f(0.0, child.size().y()),
@ -752,63 +826,79 @@ mod element {
let child_size = child.size();
let next_child_size = next_child.size();
let drag_bounds = visible_bounds.clone();
let flexes = self.flexes.clone();
let current_flex = flexes.borrow()[ix];
let flexes = self.flexes.borrow();
let current_flex = flexes[ix];
let next_ix = *next_ix;
let next_flex = flexes.borrow()[next_ix];
let next_flex = flexes[next_ix];
drop(flexes);
enum ResizeHandle {}
let mut mouse_region = MouseRegion::new::<ResizeHandle>(
cx.view_id(),
self.basis + ix,
handle_bounds,
);
mouse_region = mouse_region.on_drag(
MouseButton::Left,
move |drag, workspace: &mut Workspace, cx| {
let min_size = match axis {
Axis::Horizontal => HORIZONTAL_MIN_SIZE,
Axis::Vertical => VERTICAL_MIN_SIZE,
};
// Don't allow resizing to less than the minimum size, if elements are already too small
if min_size - 1. > child_size.along(axis)
|| min_size - 1. > next_child_size.along(axis)
{
return;
mouse_region = mouse_region
.on_drag(MouseButton::Left, {
let flexes = self.flexes.clone();
move |drag, workspace: &mut Workspace, cx| {
let min_size = match axis {
Axis::Horizontal => HORIZONTAL_MIN_SIZE,
Axis::Vertical => VERTICAL_MIN_SIZE,
};
// Don't allow resizing to less than the minimum size, if elements are already too small
if min_size - 1. > child_size.along(axis)
|| min_size - 1. > next_child_size.along(axis)
{
return;
}
let mut current_target_size =
(drag.position - child_start).along(axis);
let proposed_current_pixel_change =
current_target_size - child_size.along(axis);
if proposed_current_pixel_change < 0. {
current_target_size = f32::max(current_target_size, min_size);
} else if proposed_current_pixel_change > 0. {
// TODO: cascade this change to other children if current item is at min size
let next_target_size = f32::max(
next_child_size.along(axis) - proposed_current_pixel_change,
min_size,
);
current_target_size = f32::min(
current_target_size,
child_size.along(axis) + next_child_size.along(axis)
- next_target_size,
);
}
let current_pixel_change =
current_target_size - child_size.along(axis);
let flex_change =
current_pixel_change / drag_bounds.length_along(axis);
let current_target_flex = current_flex + flex_change;
let next_target_flex = next_flex - flex_change;
let mut borrow = flexes.borrow_mut();
*borrow.get_mut(ix).unwrap() = current_target_flex;
*borrow.get_mut(next_ix).unwrap() = next_target_flex;
workspace.schedule_serialize(cx);
cx.notify();
}
let mut current_target_size = (drag.position - child_start).along(axis);
let proposed_current_pixel_change =
current_target_size - child_size.along(axis);
if proposed_current_pixel_change < 0. {
current_target_size = f32::max(current_target_size, min_size);
} else if proposed_current_pixel_change > 0. {
// TODO: cascade this change to other children if current item is at min size
let next_target_size = f32::max(
next_child_size.along(axis) - proposed_current_pixel_change,
min_size,
);
current_target_size = f32::min(
current_target_size,
child_size.along(axis) + next_child_size.along(axis)
- next_target_size,
);
})
.on_click(MouseButton::Left, {
let flexes = self.flexes.clone();
move |e, v: &mut Workspace, cx| {
if e.click_count >= 2 {
let mut borrow = flexes.borrow_mut();
*borrow = vec![1.; borrow.len()];
v.schedule_serialize(cx);
cx.notify();
}
}
let current_pixel_change = current_target_size - child_size.along(axis);
let flex_change = current_pixel_change / drag_bounds.length_along(axis);
let current_target_flex = current_flex + flex_change;
let next_target_flex = next_flex - flex_change;
let mut borrow = flexes.borrow_mut();
*borrow.get_mut(ix).unwrap() = current_target_flex;
*borrow.get_mut(next_ix).unwrap() = next_target_flex;
workspace.schedule_serialize(cx);
cx.notify();
},
);
});
scene.push_mouse_region(mouse_region);
scene.pop_stacking_context();

View File

@ -55,26 +55,21 @@ pub trait SearchableItem: Item {
fn match_index_for_direction(
&mut self,
matches: &Vec<Self::Match>,
mut current_index: usize,
current_index: usize,
direction: Direction,
count: usize,
_: &mut ViewContext<Self>,
) -> usize {
match direction {
Direction::Prev => {
if current_index == 0 {
matches.len() - 1
let count = count % matches.len();
if current_index >= count {
current_index - count
} else {
current_index - 1
}
}
Direction::Next => {
current_index += 1;
if current_index == matches.len() {
0
} else {
current_index
matches.len() - (count - current_index)
}
}
Direction::Next => (current_index + count) % matches.len(),
}
}
fn find_matches(
@ -113,6 +108,7 @@ pub trait SearchableItemHandle: ItemHandle {
matches: &Vec<Box<dyn Any + Send>>,
current_index: usize,
direction: Direction,
count: usize,
cx: &mut WindowContext,
) -> usize;
fn find_matches(
@ -183,11 +179,12 @@ impl<T: SearchableItem> SearchableItemHandle for ViewHandle<T> {
matches: &Vec<Box<dyn Any + Send>>,
current_index: usize,
direction: Direction,
count: usize,
cx: &mut WindowContext,
) -> usize {
let matches = downcast_matches(matches);
self.update(cx, |this, cx| {
this.match_index_for_direction(&matches, current_index, direction, cx)
this.match_index_for_direction(&matches, current_index, direction, count, cx)
})
}
fn find_matches(

View File

@ -27,6 +27,7 @@ trait StatusItemViewHandle {
active_pane_item: Option<&dyn ItemHandle>,
cx: &mut WindowContext,
);
fn ui_name(&self) -> &'static str;
}
pub struct StatusBar {
@ -57,7 +58,6 @@ impl View for StatusBar {
.with_margin_right(theme.item_spacing)
}))
.into_any(),
right: Flex::row()
.with_children(self.right_items.iter().rev().map(|i| {
ChildView::new(i.as_any(), cx)
@ -96,6 +96,56 @@ impl StatusBar {
cx.notify();
}
pub fn item_of_type<T: StatusItemView>(&self) -> Option<ViewHandle<T>> {
self.left_items
.iter()
.chain(self.right_items.iter())
.find_map(|item| item.as_any().clone().downcast())
}
pub fn position_of_item<T>(&self) -> Option<usize>
where
T: StatusItemView,
{
for (index, item) in self.left_items.iter().enumerate() {
if item.as_ref().ui_name() == T::ui_name() {
return Some(index);
}
}
for (index, item) in self.right_items.iter().enumerate() {
if item.as_ref().ui_name() == T::ui_name() {
return Some(index + self.left_items.len());
}
}
return None;
}
pub fn insert_item_after<T>(
&mut self,
position: usize,
item: ViewHandle<T>,
cx: &mut ViewContext<Self>,
) where
T: 'static + StatusItemView,
{
if position < self.left_items.len() {
self.left_items.insert(position + 1, Box::new(item))
} else {
self.right_items
.insert(position + 1 - self.left_items.len(), Box::new(item))
}
cx.notify()
}
pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext<Self>) {
if position < self.left_items.len() {
self.left_items.remove(position);
} else {
self.right_items.remove(position - self.left_items.len());
}
cx.notify();
}
pub fn add_right_item<T>(&mut self, item: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T: 'static + StatusItemView,
@ -133,6 +183,10 @@ impl<T: StatusItemView> StatusItemViewHandle for ViewHandle<T> {
this.set_active_pane_item(active_pane_item, cx)
});
}
fn ui_name(&self) -> &'static str {
T::ui_name()
}
}
impl From<&dyn StatusItemViewHandle> for AnyViewHandle {

View File

@ -141,6 +141,7 @@ actions!(
ToggleLeftDock,
ToggleRightDock,
ToggleBottomDock,
CloseAllDocks,
]
);
@ -152,6 +153,9 @@ pub struct OpenPaths {
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePane(pub usize);
#[derive(Clone, Deserialize, PartialEq)]
pub struct ActivatePaneInDirection(pub SplitDirection);
#[derive(Deserialize)]
pub struct Toast {
id: usize,
@ -197,7 +201,7 @@ impl Clone for Toast {
}
}
impl_actions!(workspace, [ActivatePane, Toast]);
impl_actions!(workspace, [ActivatePane, ActivatePaneInDirection, Toast]);
pub type WorkspaceId = i64;
@ -262,6 +266,13 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
workspace.activate_next_pane(cx)
});
cx.add_action(
|workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
workspace.activate_pane_in_direction(action.0, cx)
},
);
cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
workspace.toggle_dock(DockPosition::Left, cx);
});
@ -271,6 +282,9 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
workspace.toggle_dock(DockPosition::Bottom, cx);
});
cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
workspace.close_all_docks(cx);
});
cx.add_action(Workspace::activate_pane_at_index);
cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
workspace.reopen_closed_item(cx).detach();
@ -498,7 +512,7 @@ pub struct Workspace {
follower_states_by_leader: FollowerStatesByLeader,
last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
window_edited: bool,
active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
active_call: Option<(ModelHandle<ActiveCall>, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
@ -884,6 +898,18 @@ impl Workspace {
pub fn add_panel<T: Panel>(&mut self, panel: ViewHandle<T>, cx: &mut ViewContext<Self>)
where
T::Event: std::fmt::Debug,
{
self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
}
pub fn add_panel_with_extra_event_handler<T: Panel, F>(
&mut self,
panel: ViewHandle<T>,
cx: &mut ViewContext<Self>,
handler: F,
) where
T::Event: std::fmt::Debug,
F: Fn(&mut Self, &ViewHandle<T>, &T::Event, &mut ViewContext<Self>) + 'static,
{
let dock = match panel.position(cx) {
DockPosition::Left => &self.left_dock,
@ -951,6 +977,8 @@ impl Workspace {
}
this.update_active_view_for_followers(cx);
cx.notify();
} else {
handler(this, &panel, event, cx)
}
}
}));
@ -1403,45 +1431,65 @@ impl Workspace {
// Sort the paths to ensure we add worktrees for parents before their children.
abs_paths.sort_unstable();
cx.spawn(|this, mut cx| async move {
let mut project_paths = Vec::new();
for path in &abs_paths {
if let Some(project_path) = this
let mut tasks = Vec::with_capacity(abs_paths.len());
for abs_path in &abs_paths {
let project_path = match this
.update(&mut cx, |this, cx| {
Workspace::project_path_for_path(this.project.clone(), path, visible, cx)
Workspace::project_path_for_path(
this.project.clone(),
abs_path,
visible,
cx,
)
})
.log_err()
{
project_paths.push(project_path.await.log_err());
} else {
project_paths.push(None);
}
}
Some(project_path) => project_path.await.log_err(),
None => None,
};
let tasks = abs_paths
.iter()
.cloned()
.zip(project_paths.into_iter())
.map(|(abs_path, project_path)| {
let this = this.clone();
cx.spawn(|mut cx| {
let fs = fs.clone();
async move {
let (_worktree, project_path) = project_path?;
if fs.is_file(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
this.open_path(project_path, None, true, cx)
let this = this.clone();
let task = cx.spawn(|mut cx| {
let fs = fs.clone();
let abs_path = abs_path.clone();
async move {
let (worktree, project_path) = project_path?;
if fs.is_file(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
this.open_path(project_path, None, true, cx)
})
.log_err()?
.await,
)
} else {
this.update(&mut cx, |workspace, cx| {
let worktree = worktree.read(cx);
let worktree_abs_path = worktree.abs_path();
let entry_id = if abs_path == worktree_abs_path.as_ref() {
worktree.root_entry()
} else {
abs_path
.strip_prefix(worktree_abs_path.as_ref())
.ok()
.and_then(|relative_path| {
worktree.entry_for_path(relative_path)
})
}
.map(|entry| entry.id);
if let Some(entry_id) = entry_id {
workspace.project().update(cx, |_, cx| {
cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
})
.log_err()?
.await,
)
} else {
None
}
}
})
.log_err()?;
None
}
})
})
.collect::<Vec<_>>();
}
});
tasks.push(task);
}
futures::future::join_all(tasks).await
})
@ -1660,6 +1708,20 @@ impl Workspace {
self.serialize_workspace(cx);
}
pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
for dock in docks {
dock.update(cx, |dock, cx| {
dock.set_open(false, cx);
});
}
cx.focus_self();
cx.notify();
self.serialize_workspace(cx);
}
/// Transfer focus to the panel of the given type.
pub fn focus_panel<T: Panel>(&mut self, cx: &mut ViewContext<Self>) -> Option<ViewHandle<T>> {
self.focus_or_unfocus_panel::<T>(cx, |_, _| true)?
@ -2054,6 +2116,37 @@ impl Workspace {
}
}
pub fn activate_pane_in_direction(
&mut self,
direction: SplitDirection,
cx: &mut ViewContext<Self>,
) {
let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) {
Some(coordinates) => coordinates,
None => {
return;
}
};
let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
let center = match cursor {
Some(cursor) if bounding_box.contains_point(cursor) => cursor,
_ => bounding_box.center(),
};
let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
let target = match direction {
SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
};
if let Some(pane) = self.center.pane_at_pixel_position(target) {
cx.focus(pane);
}
}
fn handle_pane_focused(&mut self, pane: ViewHandle<Pane>, cx: &mut ViewContext<Self>) {
if self.active_pane != pane {
self.active_pane = pane.clone();
@ -3030,6 +3123,7 @@ impl Workspace {
axis,
members,
flexes,
bounding_boxes: _,
}) => SerializedPaneGroup::Group {
axis: *axis,
children: members

View File

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.96.0"
version = "0.97.0"
publish = false
[lib]
@ -104,11 +104,14 @@ thiserror.workspace = true
tiny_http = "0.8"
toml.workspace = true
tree-sitter.workspace = true
tree-sitter-bash.workspace = true
tree-sitter-c.workspace = true
tree-sitter-cpp.workspace = true
tree-sitter-css.workspace = true
tree-sitter-elixir.workspace = true
tree-sitter-elm.workspace = true
tree-sitter-embedded-template.workspace = true
tree-sitter-glsl.workspace = true
tree-sitter-go.workspace = true
tree-sitter-heex.workspace = true
tree-sitter-json.workspace = true

View File

@ -40,6 +40,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
languages.register(name, load_config(name), grammar, adapters, load_queries)
};
language("bash", tree_sitter_bash::language(), vec![]);
language(
"c",
tree_sitter_c::language(),
@ -151,6 +152,8 @@ pub fn init(languages: Arc<LanguageRegistry>, node_runtime: Arc<NodeRuntime>) {
tree_sitter_php::language(),
vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))],
);
language("elm", tree_sitter_elm::language(), vec![]);
language("glsl", tree_sitter_glsl::language(), vec![]);
}
#[cfg(any(test, feature = "test-support"))]

View File

@ -0,0 +1,3 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)

View File

@ -0,0 +1,8 @@
name = "Shell Script"
path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"]
first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
brackets = [
{ start = "[", end = "]", close = true, newline = false },
{ start = "(", end = ")", close = true, newline = false },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] },
]

View File

@ -0,0 +1,58 @@
[
(string)
(raw_string)
(heredoc_body)
(heredoc_start)
] @string
(command_name) @function
(variable_name) @property
[
"case"
"do"
"done"
"elif"
"else"
"esac"
"export"
"fi"
"for"
"function"
"if"
"in"
"select"
"then"
"unset"
"until"
"while"
"local"
"declare"
] @keyword
(comment) @comment
(function_definition name: (word) @function)
(file_descriptor) @number
[
(command_substitution)
(process_substitution)
(expansion)
]@embedded
[
"$"
"&&"
">"
">>"
"<"
"|"
] @operator
(
(command (_) @constant)
(#match? @constant "^-")
)

View File

@ -0,0 +1,11 @@
name = "Elm"
path_suffixes = ["elm"]
line_comment = "-- "
block_comment = ["{- ", " -}"]
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]

View File

@ -0,0 +1,72 @@
[
"if"
"then"
"else"
"let"
"in"
(case)
(of)
(backslash)
(as)
(port)
(exposing)
(alias)
(import)
(module)
(type)
(arrow)
] @keyword
[
(eq)
(operator_identifier)
(colon)
] @operator
(type_annotation(lower_case_identifier) @function)
(port_annotation(lower_case_identifier) @function)
(function_declaration_left(lower_case_identifier) @function.definition)
(function_call_expr
target: (value_expr
name: (value_qid (lower_case_identifier) @function)))
(exposed_value(lower_case_identifier) @function)
(exposed_type(upper_case_identifier) @type)
(field_access_expr(value_expr(value_qid)) @identifier)
(lower_pattern) @variable
(record_base_identifier) @identifier
[
"("
")"
] @punctuation.bracket
[
"|"
","
] @punctuation.delimiter
(number_constant_expr) @constant
(type_declaration(upper_case_identifier) @type)
(type_ref) @type
(type_alias_declaration name: (upper_case_identifier) @type)
(value_expr(upper_case_qid(upper_case_identifier)) @type)
[
(line_comment)
(block_comment)
] @comment
(string_escape) @string.escape
[
(open_quote)
(close_quote)
(regular_string_part)
(open_char)
(close_char)
] @string

Some files were not shown because too many files have changed in this diff Show More