Detailed changes
@@ -36,11 +36,11 @@ dependencies = [
[[package]]
name = "addr2line"
-version = "0.19.0"
+version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
+checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3"
dependencies = [
- "gimli 0.27.2",
+ "gimli 0.27.3",
]
[[package]]
@@ -61,7 +61,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
- "getrandom 0.2.9",
+ "getrandom 0.2.10",
"once_cell",
"version_check",
]
@@ -88,9 +88,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
+checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
dependencies = [
"memchr",
]
@@ -118,7 +118,7 @@ dependencies = [
"settings",
"smol",
"theme",
- "tiktoken-rs 0.4.2",
+ "tiktoken-rs 0.4.5",
"util",
"workspace",
]
@@ -151,7 +151,7 @@ dependencies = [
"alacritty_config",
"alacritty_config_derive",
"base64 0.13.1",
- "bitflags",
+ "bitflags 1.3.2",
"dirs 4.0.0",
"libc",
"log",
@@ -161,7 +161,7 @@ dependencies = [
"miow 0.3.7",
"nix",
"parking_lot 0.12.1",
- "regex-automata",
+ "regex-automata 0.1.10",
"serde",
"serde_yaml",
"signal-hook",
@@ -177,12 +177,46 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+[[package]]
+name = "allocator-api2"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9"
+
+[[package]]
+name = "alsa"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44"
+dependencies = [
+ "alsa-sys",
+ "bitflags 1.3.2",
+ "libc",
+ "nix",
+]
+
+[[package]]
+name = "alsa-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
+dependencies = [
+ "libc",
+ "pkg-config",
+]
+
[[package]]
name = "ambient-authority"
version = "0.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec8ad6edb4840b78c5c3d88de606b22252d552b55f3a4699fbb10fc070ec3049"
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -203,7 +237,7 @@ dependencies = [
"anstyle-query",
"anstyle-wincon",
"colorchoice",
- "is-terminal 0.4.7",
+ "is-terminal 0.4.9",
"utf8parse",
]
@@ -228,7 +262,7 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -238,7 +272,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
dependencies = [
"anstyle",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -261,9 +295,9 @@ checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "arrayvec"
-version = "0.7.2"
+version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
+checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "ascii"
@@ -302,7 +336,7 @@ dependencies = [
"futures-core",
"futures-io",
"once_cell",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
"tokio",
]
@@ -316,7 +350,7 @@ dependencies = [
"futures-core",
"futures-io",
"memchr",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
]
[[package]]
@@ -374,7 +408,7 @@ dependencies = [
"log",
"parking",
"polling",
- "rustix 0.37.19",
+ "rustix 0.37.23",
"slab",
"socket2",
"waker-fn",
@@ -423,9 +457,9 @@ dependencies = [
"cfg-if 1.0.0",
"event-listener",
"futures-lite",
- "rustix 0.37.19",
+ "rustix 0.37.23",
"signal-hook",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -447,7 +481,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.18",
+ "syn 2.0.23",
]
[[package]]
@@ -460,7 +494,7 @@ dependencies = [
"async-global-executor",
"async-io",
"async-lock",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
"futures-channel",
"futures-core",
"futures-io",
@@ -470,7 +504,7 @@ dependencies = [
"log",
"memchr",
"once_cell",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
"pin-utils",
"slab",
"wasm-bindgen-futures",
@@ -484,7 +518,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
dependencies = [
"async-stream-impl",
"futures-core",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
]
[[package]]
@@ -495,7 +529,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.18",
+ "syn 2.0.23",
]
[[package]]
@@ -532,13 +566,13 @@ dependencies = [
[[package]]
name = "async-trait"
-version = "0.1.68"
+version = "0.1.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
+checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf"
dependencies = [
"proc-macro2",
"quote",
- "syn 2.0.18",
+ "syn 2.0.23",
]
[[package]]
@@ -551,7 +585,7 @@ dependencies = [
"futures-io",
"futures-util",
"log",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
"tungstenite 0.16.0",
]
@@ -566,12 +600,9 @@ dependencies = [
[[package]]
name = "atomic"
-version = "0.5.1"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c"
-dependencies = [
- "autocfg 1.1.0",
-]
+checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
[[package]]
name = "atomic-waker"
@@ -590,6 +621,19 @@ dependencies = [
"winapi 0.3.9",
]
+[[package]]
+name = "audio"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "gpui",
+ "log",
+ "parking_lot 0.11.2",
+ "rodio",
+ "util",
+]
+
[[package]]
name = "auto_update"
version = "0.1.0"
@@ -638,19 +682,19 @@ dependencies = [
"async-trait",
"axum-core",
"base64 0.13.1",
- "bitflags",
+ "bitflags 1.3.2",
"bytes 1.4.0",
"futures-util",
"headers",
"http",
"http-body",
"hyper",
- "itoa 1.0.6",
+ "itoa 1.0.8",
"matchit",
"memchr",
"mime",
"percent-encoding",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
"serde",
"serde_json",
"serde_urlencoded",
@@ -691,7 +735,7 @@ dependencies = [
"futures-util",
"http",
"mime",
- "pin-project-lite 0.2.9",
+ "pin-project-lite 0.2.10",
"serde",
"serde_json",
"tokio",
@@ -703,16 +747,16 @@ dependencies = [
[[package]]
name = "backtrace"
-version = "0.3.67"
+version = "0.3.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
+checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12"
dependencies = [
- "addr2line 0.19.0",
+ "addr2line 0.20.0",
"cc",
"cfg-if 1.0.0",
"libc",
- "miniz_oxide 0.6.2",
- "object 0.30.3",
+ "miniz_oxide 0.7.1",
+ "object 0.31.1",
"rustc-demangle",
]
@@ -756,13 +800,33 @@ dependencies = [
"serde",
]
+[[package]]
+name = "bindgen"
+version = "0.64.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
+dependencies = [
+ "bitflags 1.3.2",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 1.0.109",
+]
+
[[package]]
name = "bindgen"
version = "0.65.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"cexpr",
"clang-sys",
"lazy_static",
@@ -775,7 +839,7 @@ dependencies = [
"regex",
"rustc-hash",
"shlex",
- "syn 2.0.18",
+ "syn 2.0.23",
"which",
]
@@ -800,6 +864,24 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+[[package]]
+name = "bitflags"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42"
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
[[package]]
name = "block"
version = "0.1.6"
@@ -857,7 +939,7 @@ checksum = "0754613691538d51f329cce9af41d7b7ca150bc973056f1156611489475f54f7"
dependencies = [
"borsh-derive-internal",
"borsh-schema-derive-internal",
- "proc-macro-crate",
+ "proc-macro-crate 0.1.5",
"proc-macro2",
"syn 1.0.109",
]
@@ -914,27 +996,26 @@ dependencies = [
[[package]]
name = "bstr"
-version = "1.5.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5"
+checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
- "once_cell",
- "regex-automata",
+ "regex-automata 0.3.1",
"serde",
]
[[package]]
name = "bumpalo"
-version = "3.12.2"
+version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "bytecheck"
-version = "0.6.10"
+version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13fe11640a23eb24562225322cd3e452b93a3d4091d62fab69c70542fcd17d1f"
+checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627"
dependencies = [
"bytecheck_derive",
"ptr_meta",
@@ -943,9 +1024,9 @@ dependencies = [
[[package]]
name = "bytecheck_derive"
-version = "0.6.10"
+version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e31225543cb46f81a7e224762764f4a6a0f097b1db0b175f69e8065efaa42de5"
+checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61"
dependencies = [
"proc-macro2",
"quote",
@@ -986,6 +1067,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
+ "audio",
"client",
"collections",
"fs",
@@ -1082,6 +1164,12 @@ dependencies = [
"jobserver",
]
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
[[package]]
name = "cexpr"
version = "0.6.0"
@@ -1105,13 +1193,13 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
-version = "0.4.24"
+version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
dependencies = [
+ "android-tzdata",
"iana-time-zone",
"js-sys",
- "num-integer",
"num-traits",
"serde",
"time 0.1.45",
@@ -1142,7 +1230,7 @@ checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f"
dependencies = [
"glob",
"libc",
- "libloading",
+ "libloading 0.7.4",
]
[[package]]
@@ -1152,10 +1240,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
dependencies = [
"atty",
- "bitflags",
+ "bitflags 1.3.2",
"clap_derive 3.2.25",
"clap_lex 0.2.4",
- "indexmap",
+ "indexmap 1.9.3",
"once_cell",
"strsim",
"termcolor",
@@ -1164,9 +1252,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.3.5"
+version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc"
+checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
dependencies = [
"clap_builder",
"clap_derive 4.3.2",
@@ -1175,13 +1263,12 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.3.5"
+version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae"
+checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
dependencies = [
"anstream",
"anstyle",
- "bitflags",
"clap_lex 0.5.0",
"strsim",
]
@@ -1208,7 +1295,7 @@ dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
- "syn 2.0.18",
+ "syn 2.0.23",
]
[[package]]
@@ -1226,6 +1313,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
+[[package]]
+name = "claxon"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bfbf56724aa9eca8afa4fcfadeb479e722935bb2a0900c2d37e0cc477af0688"
+
[[package]]
name = "cli"
version = "0.1.0"
@@ -1269,11 +1362,11 @@ dependencies = [
"sum_tree",
"tempfile",
"thiserror",
- "time 0.3.21",
+ "time 0.3.22",
"tiny_http",
"url",
"util",
- "uuid 1.3.2",
+ "uuid 1.4.0",
]
[[package]]
@@ -1297,7 +1390,7 @@ name = "cocoa"
version = "0.24.0"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"block",
"cocoa-foundation",
"core-foundation",
@@ -1312,7 +1405,7 @@ name = "cocoa-foundation"
version = "0.1.1"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"block",
"core-foundation",
"core-graphics-types",
@@ -1321,22 +1414,13 @@ dependencies = [
"objc",
]
-[[package]]
-name = "codespan-reporting"
-version = "0.11.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
-dependencies = [
- "termcolor",
- "unicode-width",
-]
-
[[package]]
name = "collab"
-version = "0.15.0"
+version = "0.16.0"
dependencies = [
"anyhow",
"async-tungstenite",
+ "audio",
"axum",
"axum-extra",
"base64 0.13.1",
@@ -1380,7 +1464,7 @@ dependencies = [
"sha-1 0.9.8",
"sqlx",
"theme",
- "time 0.3.21",
+ "time 0.3.22",
"tokio",
"tokio-tungstenite",
"toml",
@@ -1415,6 +1499,7 @@ dependencies = [
"picker",
"postage",
"project",
+ "recent_projects",
"serde",
"serde_derive",
"settings",
@@ -1444,6 +1529,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+[[package]]
+name = "combine"
+version = "4.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
+dependencies = [
+ "bytes 1.4.0",
+ "memchr",
+]
+
[[package]]
name = "command_palette"
version = "0.1.0"
@@ -1470,7 +1565,7 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c"
dependencies = [
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
]
[[package]]
@@ -1540,11 +1635,17 @@ name = "core-foundation"
version = "0.9.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
dependencies = [
- "core-foundation-sys",
+ "core-foundation-sys 0.8.3",
"libc",
"uuid 0.5.1",
]
+[[package]]
+name = "core-foundation-sys"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
+
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
@@ -1555,7 +1656,7 @@ name = "core-graphics"
version = "0.22.3"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"core-foundation",
"core-graphics-types",
"foreign-types",
@@ -1567,7 +1668,7 @@ name = "core-graphics-types"
version = "0.1.1"
source = "git+https://github.com/servo/core-foundation-rs?rev=079665882507dd5e2ff77db3de5070c1f6c0fb85#079665882507dd5e2ff77db3de5070c1f6c0fb85"
dependencies = [
- "bitflags",
+ "bitflags 1.3.2",
"core-foundation",
"foreign-types",
"libc",
@@ -1594,6 +1695,51 @@ dependencies = [
"libc",
]
+[[package]]
+name = "coreaudio-rs"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation-sys 0.6.2",
+ "coreaudio-sys",
+]
+
+[[package]]
+name = "coreaudio-sys"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24"
+dependencies = [
+ "bindgen 0.64.0",
+]
+
+[[package]]
+name = "cpal"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c"
+dependencies = [
+ "alsa",
+ "core-foundation-sys 0.8.3",
+ "coreaudio-rs",
+ "dasp_sample",
+ "jni 0.19.0",
+ "js-sys",
+ "libc",
+ "mach2",
+ "ndk",
+ "ndk-context",
+ "oboe",
+ "once_cell",
+ "parking_lot 0.12.1",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows 0.46.0",
+]
+
[[package]]
name = "cpp_demangle"
version = "0.3.5"
@@ -1605,9 +1751,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
-version = "0.2.7"
+version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
@@ -1732,16 +1878,6 @@ dependencies = [
"cfg-if 1.0.0",
]
-[[package]]
-name = "crossbeam-channel"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
-dependencies = [
- "crossbeam-utils 0.7.2",
- "maybe-uninit",
-]
-
[[package]]
name = "crossbeam-channel"
version = "0.5.8"
@@ -1749,7 +1885,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
]
[[package]]
@@ -1760,19 +1896,19 @@ checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
-version = "0.9.14"
+version = "0.9.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
+checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
dependencies = [
"autocfg 1.1.0",
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
- "memoffset 0.8.0",
+ "crossbeam-utils",
+ "memoffset 0.9.0",
"scopeguard",
]
@@ -1783,25 +1919,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
dependencies = [
"cfg-if 1.0.0",
- "crossbeam-utils 0.8.15",
+ "crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
-version = "0.7.2"
+version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
-dependencies = [
- "autocfg 1.1.0",
- "cfg-if 0.1.10",
- "lazy_static",
-]
-
-[[package]]
-name = "crossbeam-utils"
-version = "0.8.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
+checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if 1.0.0",
]
@@ -1853,9 +1978,9 @@ dependencies = [
[[package]]
name = "curl-sys"
-version = "0.4.61+curl-8.0.1"
+version = "0.4.63+curl-8.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79"
+checksum = "aeb0fef7046022a1e2ad67a004978f0e3cacb9e3123dc62ce768f92197b771dc"
dependencies = [
"cc",
"libc",
@@ -1867,50 +1992,6 @@ dependencies = [
"winapi 0.3.9",
]
-[[package]]
-name = "cxx"
-version = "1.0.97"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88abab2f5abbe4c56e8f1fb431b784d710b709888f35755a160e62e33fe38e8"
-dependencies = [
- "cc",
- "cxxbridge-flags",
- "cxxbridge-macro",
- "link-cplusplus",
-]
-
-[[package]]
-name = "cxx-build"
-version = "1.0.94"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
-dependencies = [
- "cc",
- "codespan-reporting",
- "once_cell",
- "proc-macro2",
- "quote",
- "scratch",
- "syn 2.0.18",
-]
-
-[[package]]
-name = "cxxbridge-flags"
-version = "1.0.97"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d3816ed957c008ccd4728485511e3d9aaf7db419aa321e3d2c5a2f3411e36c8"
-
-[[package]]
-name = "cxxbridge-macro"
-version = "1.0.97"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a26acccf6f445af85ea056362561a24ef56cdc15fcc685f03aec50b9c702cb6d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.18",
-]
-
[[package]]
name = "dashmap"
version = "5.4.0"
@@ -1921,9 +2002,15 @@ dependencies = [
"hashbrown 0.12.3",
"lock_api",
"once_cell",
- "parking_lot_core 0.9.7",
+ "parking_lot_core 0.9.8",
]
+[[package]]
+name = "dasp_sample"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
+
[[package]]
name = "data-url"
version = "0.1.1"
@@ -2020,9 +2107,9 @@ dependencies = [
[[package]]
name = "digest"
-version = "0.10.6"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer 0.10.4",
"crypto-common",
@@ -2091,11 +2178,11 @@ dependencies = [
[[package]]
name = "dlib"
-version = "0.5.0"
+version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac1b7517328c04c2aa68422fc60a41b92208182142ed04a25879c26c8f878794"
+checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
- "libloading",
+ "libloading 0.8.0",
]
[[package]]
@@ -2218,7 +2305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
"humantime",
- "is-terminal 0.4.7",
+ "is-terminal 0.4.9",
"log",
"regex",
"termcolor",
@@ -2233,11 +2320,17 @@ dependencies = [
"serde",
]
+[[package]]
+name = "equivalent"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
+
[[package]]
name = "erased-serde"
-version = "0.3.25"
+version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569"
+checksum = "f94c0e13118e7d7533271f754a168ae8400e6a1cc043f2bfd53cc7290f1a1de3"
dependencies = [
"serde",
]
@@ -2261,7 +2354,7 @@ checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a"
dependencies = [
"errno-dragonfly",
"libc",
- "windows-sys 0.48.0",
+ "windows-sys",
]
[[package]]
@@ -2,6 +2,7 @@
members = [
"crates/activity_indicator",
"crates/ai",
+ "crates/audio",
"crates/auto_update",
"crates/breadcrumbs",
"crates/call",
@@ -102,6 +103,7 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" }
tree-sitter = "0.20"
unindent = { version = "0.1.7" }
+pretty_assertions = "1.3.0"
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
@@ -24,9 +24,7 @@
],
"ctrl-shift-down": "editor::AddSelectionBelow",
"ctrl-shift-up": "editor::AddSelectionAbove",
- "cmd-shift-backspace": "editor::DeleteToBeginningOfLine",
- "cmd-shift-enter": "editor::NewlineAbove",
- "cmd-enter": "editor::NewlineBelow"
+ "cmd-shift-backspace": "editor::DeleteToBeginningOfLine"
}
},
{
@@ -24,9 +24,7 @@
"ctrl-.": "editor::GoToHunk",
"ctrl-,": "editor::GoToPrevHunk",
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
- "ctrl-delete": "editor::DeleteToNextWordEnd",
- "cmd-shift-enter": "editor::NewlineAbove",
- "cmd-enter": "editor::NewlineBelow"
+ "ctrl-delete": "editor::DeleteToNextWordEnd"
}
},
{
@@ -12,8 +12,6 @@
"ctrl-shift-d": "editor::DuplicateLine",
"cmd-b": "editor::GoToDefinition",
"cmd-j": "editor::ScrollCursorCenter",
- "cmd-alt-enter": "editor::NewlineAbove",
- "cmd-enter": "editor::NewlineBelow",
"cmd-shift-l": "editor::SelectLine",
"cmd-shift-t": "outline::Toggle",
"alt-backspace": "editor::DeleteToPreviousWordStart",
@@ -56,7 +54,9 @@
},
{
"context": "Editor && mode == full",
- "bindings": {}
+ "bindings": {
+ "cmd-alt-enter": "editor::NewlineAbove"
+ }
},
{
"context": "BufferSearchBar",
@@ -71,15 +71,17 @@
// "never"
"show": "auto",
// Whether to show git diff indicators in the scrollbar.
- "git_diff": true
+ "git_diff": true,
+ // Whether to show selections in the scrollbar.
+ "selections": true
},
// Inlay hint related settings
"inlay_hints": {
// Global switch to toggle hints on and off, switched off by default.
- "enabled": false,
+ "enabled": false,
// Toggle certain types of hints on and off, all switched on by default.
"show_type_hints": true,
- "show_parameter_hints": true,
+ "show_parameter_hints": true,
// Corresponds to null/None LSP hint type value.
"show_other_hints": true
},
@@ -12,6 +12,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{
cmp::Reverse,
+ ffi::OsStr,
fmt::{self, Display},
path::PathBuf,
sync::Arc,
@@ -80,6 +81,9 @@ impl SavedConversationMetadata {
let mut conversations = Vec::<SavedConversationMetadata>::new();
while let Some(path) = paths.next().await {
let path = path?;
+ if path.extension() != Some(OsStr::new("json")) {
+ continue;
+ }
let pattern = r" - \d+.zed.json$";
let re = Regex::new(pattern).unwrap();
@@ -147,8 +147,9 @@ impl AssistantPanel {
.await
.log_err()
.unwrap_or_default();
- this.update(&mut cx, |this, _| {
- this.saved_conversations = saved_conversations
+ this.update(&mut cx, |this, cx| {
+ this.saved_conversations = saved_conversations;
+ cx.notify();
})
.ok();
}
@@ -1911,7 +1912,7 @@ impl ConversationEditor {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
- let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else {
+ let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::<Editor>(cx)) else {
return;
};
@@ -0,0 +1,23 @@
+[package]
+name = "audio"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/audio.rs"
+doctest = false
+
+[dependencies]
+gpui = { path = "../gpui" }
+collections = { path = "../collections" }
+util = { path = "../util" }
+
+rodio = "0.17.1"
+
+log.workspace = true
+
+anyhow.workspace = true
+parking_lot.workspace = true
+
+[dev-dependencies]
@@ -0,0 +1,44 @@
+use std::{io::Cursor, sync::Arc};
+
+use anyhow::Result;
+use collections::HashMap;
+use gpui::{AppContext, AssetSource};
+use rodio::{
+ source::{Buffered, SamplesConverter},
+ Decoder, Source,
+};
+
+type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
+
+pub struct SoundRegistry {
+ cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
+ assets: Box<dyn AssetSource>,
+}
+
+impl SoundRegistry {
+ pub fn new(source: impl AssetSource) -> Arc<Self> {
+ Arc::new(Self {
+ cache: Default::default(),
+ assets: Box::new(source),
+ })
+ }
+
+ pub fn global(cx: &AppContext) -> Arc<Self> {
+ cx.global::<Arc<Self>>().clone()
+ }
+
+ pub fn get(&self, name: &str) -> Result<impl Source<Item = f32>> {
+ if let Some(wav) = self.cache.lock().get(name) {
+ return Ok(wav.clone());
+ }
+
+ let path = format!("sounds/{}.wav", name);
+ let bytes = self.assets.load(&path)?.into_owned();
+ let cursor = Cursor::new(bytes);
+ let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
+
+ self.cache.lock().insert(name.to_string(), source.clone());
+
+ Ok(source)
+ }
+}
@@ -0,0 +1,67 @@
+use assets::SoundRegistry;
+use gpui::{AppContext, AssetSource};
+use rodio::{OutputStream, OutputStreamHandle};
+use util::ResultExt;
+
+mod assets;
+
+pub fn init(source: impl AssetSource, cx: &mut AppContext) {
+ cx.set_global(SoundRegistry::new(source));
+ cx.set_global(Audio::new());
+}
+
+pub enum Sound {
+ Joined,
+ Leave,
+ Mute,
+ Unmute,
+ StartScreenshare,
+ StopScreenshare,
+}
+
+impl Sound {
+ fn file(&self) -> &'static str {
+ match self {
+ Self::Joined => "joined_call",
+ Self::Leave => "leave_call",
+ Self::Mute => "mute",
+ Self::Unmute => "unmute",
+ Self::StartScreenshare => "start_screenshare",
+ Self::StopScreenshare => "stop_screenshare",
+ }
+ }
+}
+
+pub struct Audio {
+ _output_stream: Option<OutputStream>,
+ output_handle: Option<OutputStreamHandle>,
+}
+
+impl Audio {
+ pub fn new() -> Self {
+ let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
+
+ Self {
+ _output_stream,
+ output_handle,
+ }
+ }
+
+ pub fn play_sound(sound: Sound, cx: &AppContext) {
+ if !cx.has_global::<Self>() {
+ return;
+ }
+
+ let this = cx.global::<Self>();
+
+ let Some(output_handle) = this.output_handle.as_ref() else {
+ return;
+ };
+
+ let Some(source) = SoundRegistry::global(cx).get(sound.file()).log_err() else {
+ return;
+ };
+
+ output_handle.play_raw(source).log_err();
+ }
+}
@@ -19,6 +19,7 @@ test-support = [
]
[dependencies]
+audio = { path = "../audio" }
client = { path = "../client" }
collections = { path = "../collections" }
gpui = { path = "../gpui" }
@@ -3,6 +3,7 @@ use crate::{
IncomingCall,
};
use anyhow::{anyhow, Result};
+use audio::{Audio, Sound};
use client::{
proto::{self, PeerId},
Client, TypedEnvelope, User, UserStore,
@@ -151,6 +152,7 @@ impl Room {
let connect = room.connect(&connection_info.server_url, &connection_info.token);
cx.spawn(|this, mut cx| async move {
connect.await?;
+
this.update(&mut cx, |this, cx| this.share_microphone(cx))
.await?;
@@ -176,6 +178,8 @@ impl Room {
let maintain_connection =
cx.spawn_weak(|this, cx| Self::maintain_connection(this, client.clone(), cx).log_err());
+ Audio::play_sound(Sound::Joined, cx);
+
Self {
id,
live_kit: live_kit_room,
@@ -265,6 +269,7 @@ impl Room {
room.apply_room_update(room_proto, cx)?;
anyhow::Ok(())
})?;
+
Ok(room)
})
}
@@ -306,6 +311,8 @@ impl Room {
}
}
+ Audio::play_sound(Sound::Leave, cx);
+
self.status = RoomStatus::Offline;
self.remote_participants.clear();
self.pending_participants.clear();
@@ -656,6 +663,8 @@ impl Room {
},
);
+ Audio::play_sound(Sound::Joined, cx);
+
if let Some(live_kit) = this.live_kit.as_ref() {
let video_tracks =
live_kit.room.remote_video_tracks(&user.id.to_string());
@@ -922,6 +931,7 @@ impl Room {
cx.spawn(|this, mut cx| async move {
let project =
Project::remote(id, client, user_store, language_registry, fs, cx.clone()).await?;
+
this.update(&mut cx, |this, cx| {
this.joined_projects.retain(|project| {
if let Some(project) = project.upgrade(cx) {
@@ -1212,6 +1222,9 @@ impl Room {
};
cx.notify();
}
+
+ Audio::play_sound(Sound::StartScreenshare, cx);
+
Ok(())
}
Err(error) => {
@@ -1227,38 +1240,20 @@ impl Room {
})
})
}
- fn set_mute(
- live_kit: &mut LiveKitRoom,
- should_mute: bool,
- cx: &mut ModelContext<Self>,
- ) -> Result<Task<Result<()>>> {
- if !should_mute {
- // clear user muting state.
- live_kit.muted_by_user = false;
- }
- match &mut live_kit.microphone_track {
- LocalTrack::None => Err(anyhow!("microphone was not shared")),
- LocalTrack::Pending { muted, .. } => {
- *muted = should_mute;
- cx.notify();
- Ok(Task::Ready(Some(Ok(()))))
- }
- LocalTrack::Published {
- track_publication,
- muted,
- } => {
- *muted = should_mute;
- cx.notify();
- Ok(cx.background().spawn(track_publication.set_mute(*muted)))
- }
- }
- }
+
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() {
- let ret = Self::set_mute(live_kit, should_mute, cx);
+ let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?;
live_kit.muted_by_user = should_mute;
- ret
+
+ if old_muted == true && live_kit.deafened == true {
+ if let Some(task) = self.toggle_deafen(cx).ok() {
+ task.detach();
+ }
+ }
+
+ Ok(ret_task)
} else {
Err(anyhow!("LiveKit not started"))
}
@@ -1274,7 +1269,7 @@ impl Room {
// When deafening, mute user's mic as well.
// When undeafening, unmute user's mic unless it was manually muted prior to deafening.
if live_kit.deafened || !live_kit.muted_by_user {
- mute_task = Some(Self::set_mute(live_kit, live_kit.deafened, cx)?);
+ mute_task = Some(live_kit.set_mute(live_kit.deafened, cx)?.0);
};
for participant in self.remote_participants.values() {
for track in live_kit
@@ -1319,6 +1314,8 @@ impl Room {
} => {
live_kit.room.unpublish_track(track_publication);
cx.notify();
+
+ Audio::play_sound(Sound::StopScreenshare, cx);
Ok(())
}
}
@@ -1347,6 +1344,51 @@ struct LiveKitRoom {
_maintain_tracks: [Task<()>; 2],
}
+impl LiveKitRoom {
+ fn set_mute(
+ self: &mut LiveKitRoom,
+ should_mute: bool,
+ cx: &mut ModelContext<Room>,
+ ) -> Result<(Task<Result<()>>, bool)> {
+ if !should_mute {
+ // clear user muting state.
+ self.muted_by_user = false;
+ }
+
+ let (result, old_muted) = match &mut self.microphone_track {
+ LocalTrack::None => Err(anyhow!("microphone was not shared")),
+ LocalTrack::Pending { muted, .. } => {
+ let old_muted = *muted;
+ *muted = should_mute;
+ cx.notify();
+ Ok((Task::Ready(Some(Ok(()))), old_muted))
+ }
+ LocalTrack::Published {
+ track_publication,
+ muted,
+ } => {
+ let old_muted = *muted;
+ *muted = should_mute;
+ cx.notify();
+ Ok((
+ cx.background().spawn(track_publication.set_mute(*muted)),
+ old_muted,
+ ))
+ }
+ }?;
+
+ if old_muted != should_mute {
+ if should_mute {
+ Audio::play_sound(Sound::Mute, cx);
+ } else {
+ Audio::play_sound(Sound::Unmute, cx);
+ }
+ }
+
+ Ok((result, old_muted))
+ }
+}
+
enum LocalTrack {
None,
Pending {
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.15.0"
+version = "0.16.0"
publish = false
[[bin]]
@@ -57,6 +57,7 @@ tracing-log = "0.1.3"
tracing-subscriber = { version = "0.3.11", features = ["env-filter", "json"] }
[dev-dependencies]
+audio = { path = "../audio" }
collections = { path = "../collections", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
call = { path = "../call", features = ["test-support"] }
@@ -67,7 +68,7 @@ fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
@@ -3517,7 +3517,6 @@ pub use test::*;
mod test {
use super::*;
use gpui::executor::Background;
- use lazy_static::lazy_static;
use parking_lot::Mutex;
use sea_orm::ConnectionTrait;
use sqlx::migrate::MigrateDatabase;
@@ -3566,9 +3565,7 @@ mod test {
}
pub fn postgres(background: Arc<Background>) -> Self {
- lazy_static! {
- static ref LOCK: Mutex<()> = Mutex::new(());
- }
+ static LOCK: Mutex<()> = Mutex::new(());
let _guard = LOCK.lock();
let mut rng = StdRng::from_entropy();
@@ -203,6 +203,7 @@ impl TestServer {
language::init(cx);
editor::init_settings(cx);
workspace::init(app_state.clone(), cx);
+ audio::init((), cx);
call::init(client.clone(), user_store.clone(), cx);
});
@@ -18,7 +18,7 @@ use gpui::{
};
use indoc::indoc;
use language::{
- language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings},
+ language_settings::{AllLanguageSettings, Formatter, InlayHintSettings},
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
LanguageConfig, OffsetRangeExt, Point, Rope,
};
@@ -7843,7 +7843,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
});
});
});
- let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
let mut language = Language::new(
LanguageConfig {
@@ -7955,10 +7954,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Host should get its first hints when opens an editor"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Cache should use editor settings to get the allowed hint kinds"
- );
assert_eq!(
inlay_cache.version, edits_made,
"Host editor update the cache version after every cache/view change",
@@ -7982,10 +7977,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Client should get its first hints when opens an editor"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Cache should use editor settings to get the allowed hint kinds"
- );
assert_eq!(
inlay_cache.version, edits_made,
"Guest editor update the cache version after every cache/view change"
@@ -8007,10 +7998,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Host should get hints from the 1st edit and 1st LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Inlay kinds settings never change during the test"
- );
assert_eq!(inlay_cache.version, edits_made);
});
editor_b.update(cx_b, |editor, _| {
@@ -8025,10 +8012,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Guest should get hints the 1st edit and 2nd LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Inlay kinds settings never change during the test"
- );
assert_eq!(inlay_cache.version, edits_made);
});
@@ -8054,10 +8037,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
4th query was made by guest (but not applied) due to cache invalidation logic"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Inlay kinds settings never change during the test"
- );
assert_eq!(inlay_cache.version, edits_made);
});
editor_b.update(cx_b, |editor, _| {
@@ -8074,10 +8053,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Guest should get hints from 3rd edit, 6th LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Inlay kinds settings never change during the test"
- );
assert_eq!(inlay_cache.version, edits_made);
});
@@ -8103,10 +8078,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Host should react to /refresh LSP request and get new hints from 7th LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Inlay kinds settings never change during the test"
- );
assert_eq!(
inlay_cache.version, edits_made,
"Host should accepted all edits and bump its cache version every time"
@@ -8128,10 +8099,6 @@ async fn test_mutual_editor_inlay_hint_cache_update(
"Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Inlay kinds settings never change during the test"
- );
assert_eq!(
inlay_cache.version,
edits_made,
@@ -8164,9 +8131,9 @@ async fn test_inlay_hint_refresh_is_forwarded(
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: false,
- show_type_hints: true,
+ show_type_hints: false,
show_parameter_hints: false,
- show_other_hints: true,
+ show_other_hints: false,
})
});
});
@@ -8177,13 +8144,12 @@ async fn test_inlay_hint_refresh_is_forwarded(
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
show_type_hints: true,
- show_parameter_hints: false,
+ show_parameter_hints: true,
show_other_hints: true,
})
});
});
});
- let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
let mut language = Language::new(
LanguageConfig {
@@ -8299,10 +8265,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Host should get no hints due to them turned off"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Host should have allowed hint kinds set despite hints are off"
- );
assert_eq!(
inlay_cache.version, 0,
"Host should not increment its cache version due to no changes",
@@ -8318,10 +8280,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Client should get its first hints when opens an editor"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Cache should use editor settings to get the allowed hint kinds"
- );
assert_eq!(
inlay_cache.version, edits_made,
"Guest editor update the cache version after every cache/view change"
@@ -8339,7 +8297,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Host should get nop hints due to them turned off, even after the /refresh"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(
inlay_cache.version, 0,
"Host should not increment its cache version due to no changes",
@@ -8355,10 +8312,6 @@ async fn test_inlay_hint_refresh_is_forwarded(
"Guest should get a /refresh LSP request propagated by host despite host hints are off"
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(
- inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
- "Inlay kinds settings never change during the test"
- );
assert_eq!(
inlay_cache.version, edits_made,
"Guest should accepted all edits and bump its cache version every time"
@@ -37,9 +37,9 @@ use util::ResultExt;
lazy_static::lazy_static! {
static ref PLAN_LOAD_PATH: Option<PathBuf> = path_env_var("LOAD_PLAN");
static ref PLAN_SAVE_PATH: Option<PathBuf> = path_env_var("SAVE_PLAN");
- static ref LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Default::default();
- static ref PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Default::default();
}
+static LOADED_PLAN_JSON: Mutex<Option<Vec<u8>>> = Mutex::new(None);
+static PLAN: Mutex<Option<Arc<Mutex<TestPlan>>>> = Mutex::new(None);
#[gpui::test(iterations = 100, on_failure = "on_failure")]
async fn test_random_collaboration(
@@ -35,6 +35,7 @@ gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
+recent_projects = {path = "../recent_projects"}
settings = { path = "../settings" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
@@ -42,6 +43,7 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
zed-actions = {path = "../zed-actions"}
+
anyhow.workspace = true
futures.workspace = true
log.workspace = true
@@ -0,0 +1,238 @@
+use anyhow::{anyhow, bail};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::{ops::Not, sync::Arc};
+use util::ResultExt;
+use workspace::{Toast, Workspace};
+
+pub fn init(cx: &mut AppContext) {
+ Picker::<BranchListDelegate>::init(cx);
+}
+
+pub type BranchList = Picker<BranchListDelegate>;
+
+pub fn build_branch_list(
+ workspace: ViewHandle<Workspace>,
+ cx: &mut ViewContext<BranchList>,
+) -> BranchList {
+ Picker::new(
+ BranchListDelegate {
+ matches: vec![],
+ workspace,
+ selected_index: 0,
+ last_query: String::default(),
+ },
+ cx,
+ )
+ .with_theme(|theme| theme.picker.clone())
+}
+
+pub struct BranchListDelegate {
+ matches: Vec<StringMatch>,
+ workspace: ViewHandle<Workspace>,
+ selected_index: usize,
+ last_query: String,
+}
+
+impl PickerDelegate for BranchListDelegate {
+ fn placeholder_text(&self) -> Arc<str> {
+ "Select branch...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+ cx.spawn(move |picker, mut cx| async move {
+ let Some(candidates) = picker
+ .read_with(&mut cx, |view, cx| {
+ let delegate = view.delegate();
+ let project = delegate.workspace.read(cx).project().read(&cx);
+ let mut cwd =
+ project
+ .visible_worktrees(cx)
+ .next()
+ .unwrap()
+ .read(cx)
+ .abs_path()
+ .to_path_buf();
+ cwd.push(".git");
+ let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")};
+ let mut branches = repo
+ .lock()
+ .branches()?;
+ const RECENT_BRANCHES_COUNT: usize = 10;
+ if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
+ // Truncate list of recent branches
+ // Do a partial sort to show recent-ish branches first.
+ branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
+ rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
+ });
+ branches.truncate(RECENT_BRANCHES_COUNT);
+ branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+ }
+ Ok(branches
+ .iter()
+ .cloned()
+ .enumerate()
+ .map(|(ix, command)| StringMatchCandidate {
+ id: ix,
+ char_bag: command.name.chars().collect(),
+ string: command.name.into(),
+ })
+ .collect::<Vec<_>>())
+ })
+ .log_err() else { return; };
+ let Some(candidates) = candidates.log_err() else {return;};
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ 10000,
+ &Default::default(),
+ cx.background(),
+ )
+ .await
+ };
+ picker
+ .update(&mut cx, |picker, _| {
+ let delegate = picker.delegate_mut();
+ delegate.matches = matches;
+ if delegate.matches.is_empty() {
+ delegate.selected_index = 0;
+ } else {
+ delegate.selected_index =
+ core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
+ }
+ delegate.last_query = query;
+ })
+ .log_err();
+ })
+ }
+
+ fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ let current_pick = self.selected_index();
+ let current_pick = self.matches[current_pick].string.clone();
+ cx.spawn(|picker, mut cx| async move {
+ picker.update(&mut cx, |this, cx| {
+ let project = this.delegate().workspace.read(cx).project().read(cx);
+ let mut cwd = project
+ .visible_worktrees(cx)
+ .next()
+ .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+ .read(cx)
+ .abs_path()
+ .to_path_buf();
+ cwd.push(".git");
+ let status = project
+ .fs()
+ .open_repo(&cwd)
+ .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
+ .lock()
+ .change_branch(¤t_pick);
+ if status.is_err() {
+ const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
+ this.delegate().workspace.update(cx, |model, ctx| {
+ model.show_toast(
+ Toast::new(
+ GIT_CHECKOUT_FAILURE_ID,
+ format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
+ ),
+ ctx,
+ )
+ });
+ status?;
+ }
+ cx.emit(PickerEvent::Dismiss);
+
+ Ok::<(), anyhow::Error>(())
+ }).log_err();
+ }).detach();
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ cx.emit(PickerEvent::Dismiss);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ mouse_state: &mut MouseState,
+ selected: bool,
+ cx: &gpui::AppContext,
+ ) -> AnyElement<Picker<Self>> {
+ const DISPLAYED_MATCH_LEN: usize = 29;
+ let theme = &theme::current(cx);
+ let hit = &self.matches[ix];
+ let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
+ let highlights = hit
+ .positions
+ .iter()
+ .copied()
+ .filter(|index| index < &DISPLAYED_MATCH_LEN)
+ .collect();
+ let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+ Flex::row()
+ .with_child(
+ Label::new(shortened_branch_name.clone(), style.label.clone())
+ .with_highlights(highlights)
+ .contained()
+ .aligned()
+ .left(),
+ )
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_height(theme.contact_finder.row_height)
+ .into_any()
+ }
+ fn render_header(
+ &self,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<AnyElement<Picker<Self>>> {
+ let theme = &theme::current(cx);
+ let style = theme.picker.header.clone();
+ let label = if self.last_query.is_empty() {
+ Flex::row()
+ .with_child(Label::new("Recent branches", style.label.clone()))
+ .contained()
+ .with_style(style.container)
+ } else {
+ Flex::row()
+ .with_child(Label::new("Branches", style.label.clone()))
+ .with_children(self.matches.is_empty().not().then(|| {
+ let suffix = if self.matches.len() == 1 { "" } else { "es" };
+ Label::new(
+ format!("{} match{}", self.matches.len(), suffix),
+ style.label,
+ )
+ .flex_float()
+ }))
+ .contained()
+ .with_style(style.container)
+ };
+ Some(label.into_any())
+ }
+}
@@ -1,5 +1,8 @@
use crate::{
- contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
+ branch_list::{build_branch_list, BranchList},
+ contact_notification::ContactNotification,
+ contacts_popover,
+ face_pile::FacePile,
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
ToggleScreenSharing,
};
@@ -18,19 +21,25 @@ use gpui::{
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle,
};
+use picker::PickerEvent;
use project::{Project, RepositoryEntry};
+use recent_projects::{build_recent_projects, RecentProjects};
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
-use workspace::{FollowNextCollaborator, Workspace};
+use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
-// const MAX_TITLE_LENGTH: usize = 75;
+const MAX_PROJECT_NAME_LENGTH: usize = 40;
+const MAX_BRANCH_NAME_LENGTH: usize = 40;
actions!(
collab,
[
ToggleContactsMenu,
ToggleUserMenu,
+ ToggleVcsMenu,
+ ToggleProjectMenu,
+ SwitchBranch,
ShareProject,
UnshareProject,
]
@@ -41,6 +50,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::toggle_user_menu);
+ cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+ cx.add_action(CollabTitlebarItem::toggle_project_menu);
}
pub struct CollabTitlebarItem {
@@ -49,6 +60,8 @@ pub struct CollabTitlebarItem {
client: Arc<Client>,
workspace: WeakViewHandle<Workspace>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
+ branch_popover: Option<ViewHandle<BranchList>>,
+ project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
@@ -69,12 +82,11 @@ impl View for CollabTitlebarItem {
return Empty::new().into_any();
};
- let project = self.project.read(cx);
let theme = theme::current(cx).clone();
let mut left_container = Flex::row();
let mut right_container = Flex::row().align_children_center();
- left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
+ left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
let user = self.user_store.read(cx).current_user();
let peer_id = self.client.peer_id();
@@ -182,52 +194,97 @@ impl CollabTitlebarItem {
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
+ branch_popover: None,
+ project_popover: None,
_subscriptions: subscriptions,
}
}
fn collect_title_root_names(
&self,
- project: &Project,
theme: Arc<Theme>,
- cx: &ViewContext<Self>,
+ cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
- let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
- let worktree = worktree.read(cx);
- (worktree.root_name(), worktree.root_git_entry())
- });
+ let project = self.project.read(cx);
- let (name, entry) = names_and_branches.next().unwrap_or(("", None));
+ let (name, entry) = {
+ let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+ let worktree = worktree.read(cx);
+ (worktree.root_name(), worktree.root_git_entry())
+ });
+
+ names_and_branches.next().unwrap_or(("", None))
+ };
+
+ let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
let branch_prepended = entry
.as_ref()
.and_then(RepositoryEntry::branch)
- .map(|branch| format!("/{branch}"));
- let text_style = theme.titlebar.title.clone();
+ .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
+ let project_style = theme.titlebar.project_menu_button.clone();
+ let git_style = theme.titlebar.git_menu_button.clone();
+ let divider_style = theme.titlebar.project_name_divider.clone();
let item_spacing = theme.titlebar.item_spacing;
- let mut highlight = text_style.clone();
- highlight.color = theme.titlebar.highlight_color;
-
- let style = LabelStyle {
- text: text_style,
- highlight_text: Some(highlight),
- };
let mut ret = Flex::row().with_child(
- Label::new(name.to_owned(), style.clone())
- .with_highlights((0..name.len()).into_iter().collect())
- .contained()
- .aligned()
- .left()
- .into_any_named("title-project-name"),
+ Stack::new()
+ .with_child(
+ MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
+ let style = project_style
+ .in_state(self.project_popover.is_some())
+ .style_for(mouse_state);
+ Label::new(name, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .aligned()
+ .left()
+ .into_any_named("title-project-name")
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, this, cx| {
+ this.toggle_project_menu(&Default::default(), cx)
+ })
+ .on_click(MouseButton::Left, move |_, _, _| {}),
+ )
+ .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
);
if let Some(git_branch) = branch_prepended {
ret = ret.with_child(
- Label::new(git_branch, style)
- .contained()
- .with_margin_right(item_spacing)
- .aligned()
- .left()
- .into_any_named("title-project-branch"),
+ Flex::row()
+ .with_child(
+ Label::new("/", divider_style.text)
+ .contained()
+ .with_style(divider_style.container)
+ .aligned()
+ .left(),
+ )
+ .with_child(
+ Stack::new()
+ .with_child(
+ MouseEventHandler::<ToggleVcsMenu, Self>::new(
+ 0,
+ cx,
+ |mouse_state, _| {
+ let style = git_style
+ .in_state(self.branch_popover.is_some())
+ .style_for(mouse_state);
+ Label::new(git_branch, style.text.clone())
+ .contained()
+ .with_style(style.container.clone())
+ .with_margin_right(item_spacing)
+ .aligned()
+ .left()
+ .into_any_named("title-project-branch")
+ },
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, this, cx| {
+ this.toggle_vcs_menu(&Default::default(), cx)
+ })
+ .on_click(MouseButton::Left, move |_, _, _| {}),
+ )
+ .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+ ),
)
}
ret.into_any()
@@ -320,7 +377,135 @@ impl CollabTitlebarItem {
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
});
}
+ fn render_branches_popover_host<'a>(
+ &'a self,
+ _theme: &'a theme::Titlebar,
+ cx: &'a mut ViewContext<Self>,
+ ) -> Option<AnyElement<Self>> {
+ self.branch_popover.as_ref().map(|child| {
+ let theme = theme::current(cx).clone();
+ let child = ChildView::new(child, cx);
+ let child = MouseEventHandler::<BranchList, Self>::new(0, cx, |_, _| {
+ child
+ .flex(1., true)
+ .contained()
+ .constrained()
+ .with_width(theme.contacts_popover.width)
+ .with_height(theme.contacts_popover.height)
+ })
+ .on_click(MouseButton::Left, |_, _, _| {})
+ .on_down_out(MouseButton::Left, move |_, this, cx| {
+ this.branch_popover.take();
+ cx.emit(());
+ cx.notify();
+ })
+ .contained()
+ .into_any();
+ Overlay::new(child)
+ .with_fit_mode(OverlayFitMode::SwitchAnchor)
+ .with_anchor_corner(AnchorCorner::TopLeft)
+ .with_z_index(999)
+ .aligned()
+ .bottom()
+ .left()
+ .into_any()
+ })
+ }
+ fn render_project_popover_host<'a>(
+ &'a self,
+ _theme: &'a theme::Titlebar,
+ cx: &'a mut ViewContext<Self>,
+ ) -> Option<AnyElement<Self>> {
+ self.project_popover.as_ref().map(|child| {
+ let theme = theme::current(cx).clone();
+ let child = ChildView::new(child, cx);
+ let child = MouseEventHandler::<RecentProjects, Self>::new(0, cx, |_, _| {
+ child
+ .flex(1., true)
+ .contained()
+ .constrained()
+ .with_width(theme.contacts_popover.width)
+ .with_height(theme.contacts_popover.height)
+ })
+ .on_click(MouseButton::Left, |_, _, _| {})
+ .on_down_out(MouseButton::Left, move |_, this, cx| {
+ this.project_popover.take();
+ cx.emit(());
+ cx.notify();
+ })
+ .into_any();
+
+ Overlay::new(child)
+ .with_fit_mode(OverlayFitMode::SwitchAnchor)
+ .with_anchor_corner(AnchorCorner::TopLeft)
+ .with_z_index(999)
+ .aligned()
+ .bottom()
+ .left()
+ .into_any()
+ })
+ }
+ pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+ if self.branch_popover.take().is_none() {
+ if let Some(workspace) = self.workspace.upgrade(cx) {
+ let view = cx.add_view(|cx| build_branch_list(workspace, cx));
+ cx.subscribe(&view, |this, _, event, cx| {
+ match event {
+ PickerEvent::Dismiss => {
+ this.branch_popover = None;
+ }
+ }
+
+ cx.notify();
+ })
+ .detach();
+ self.project_popover.take();
+ cx.focus(&view);
+ self.branch_popover = Some(view);
+ }
+ }
+
+ cx.notify();
+ }
+
+ pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
+ let workspace = self.workspace.clone();
+ if self.project_popover.take().is_none() {
+ cx.spawn(|this, mut cx| async move {
+ let workspaces = WORKSPACE_DB
+ .recent_workspaces_on_disk()
+ .await
+ .unwrap_or_default()
+ .into_iter()
+ .map(|(_, location)| location)
+ .collect();
+
+ let workspace = workspace.clone();
+ this.update(&mut cx, move |this, cx| {
+ let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
+
+ cx.subscribe(&view, |this, _, event, cx| {
+ match event {
+ PickerEvent::Dismiss => {
+ this.project_popover = None;
+ }
+ }
+
+ cx.notify();
+ })
+ .detach();
+ cx.focus(&view);
+ this.branch_popover.take();
+ this.project_popover = Some(view);
+ cx.notify();
+ })
+ .log_err();
+ })
+ .detach();
+ }
+ cx.notify();
+ }
fn render_toggle_contacts_button(
&self,
theme: &Theme,
@@ -733,7 +918,7 @@ impl CollabTitlebarItem {
self.contacts_popover.as_ref().map(|popover| {
Overlay::new(ChildView::new(popover, cx))
.with_fit_mode(OverlayFitMode::SwitchAnchor)
- .with_anchor_corner(AnchorCorner::TopRight)
+ .with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
@@ -1,3 +1,4 @@
+mod branch_list;
mod collab_titlebar_item;
mod contact_finder;
mod contact_list;
@@ -28,6 +29,7 @@ actions!(
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+ branch_list::init(cx);
collab_titlebar_item::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
@@ -244,8 +244,7 @@ impl ContextMenu {
let show_count = self.show_count;
cx.defer(move |this, cx| {
if cx.handle().is_focused(cx) && this.show_count == show_count {
- let window_id = cx.window_id();
- (**cx).focus(window_id, this.previously_focused_view_id.take());
+ (**cx).focus(this.previously_focused_view_id.take());
}
});
} else {
@@ -43,10 +43,10 @@ const DB_FILE_NAME: &'static str = "db.sqlite";
lazy_static::lazy_static! {
// !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING
static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty());
- static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
pub static ref BACKUP_DB_PATH: RwLock<Option<PathBuf>> = RwLock::new(None);
pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false);
}
+static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
/// Open or create a database at the given directory path.
/// This will retry a couple times if there are failures. If opening fails once, the db directory
@@ -20,7 +20,6 @@ use language::{
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
use sum_tree::{Bias, TreeMap};
use tab_map::TabMap;
-use text::Rope;
use wrap_map::WrapMap;
pub use block_map::{
@@ -28,7 +27,7 @@ pub use block_map::{
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
};
-pub use self::inlay_map::{Inlay, InlayProperties};
+pub use self::inlay_map::Inlay;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum FoldStatus {
@@ -246,10 +245,10 @@ impl DisplayMap {
self.inlay_map.current_inlays()
}
- pub fn splice_inlays<T: Into<Rope>>(
+ pub fn splice_inlays(
&mut self,
to_remove: Vec<InlayId>,
- to_insert: Vec<(InlayId, InlayProperties<T>)>,
+ to_insert: Vec<Inlay>,
cx: &mut ModelContext<Self>,
) {
if to_remove.is_empty() && to_insert.is_empty() {
@@ -2,9 +2,9 @@ use crate::{
multi_buffer::{MultiBufferChunks, MultiBufferRows},
Anchor, InlayId, MultiBufferSnapshot, ToOffset,
};
-use collections::{BTreeMap, BTreeSet, HashMap};
+use collections::{BTreeMap, BTreeSet};
use gpui::fonts::HighlightStyle;
-use language::{Chunk, Edit, Point, Rope, TextSummary};
+use language::{Chunk, Edit, Point, TextSummary};
use std::{
any::TypeId,
cmp,
@@ -13,13 +13,12 @@ use std::{
vec,
};
use sum_tree::{Bias, Cursor, SumTree};
-use text::Patch;
+use text::{Patch, Rope};
use super::TextHighlights;
pub struct InlayMap {
snapshot: InlaySnapshot,
- inlays_by_id: HashMap<InlayId, Inlay>,
inlays: Vec<Inlay>,
}
@@ -43,10 +42,29 @@ pub struct Inlay {
pub text: text::Rope,
}
-#[derive(Debug, Clone)]
-pub struct InlayProperties<T> {
- pub position: Anchor,
- pub text: T,
+impl Inlay {
+ pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
+ let mut text = hint.text();
+ if hint.padding_right && !text.ends_with(' ') {
+ text.push(' ');
+ }
+ if hint.padding_left && !text.starts_with(' ') {
+ text.insert(0, ' ');
+ }
+ Self {
+ id: InlayId::Hint(id),
+ position,
+ text: text.into(),
+ }
+ }
+
+ pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+ Self {
+ id: InlayId::Suggestion(id),
+ position,
+ text: text.into(),
+ }
+ }
}
impl sum_tree::Item for Transform {
@@ -368,7 +386,6 @@ impl InlayMap {
(
Self {
snapshot: snapshot.clone(),
- inlays_by_id: HashMap::default(),
inlays: Vec::new(),
},
snapshot,
@@ -510,45 +527,40 @@ impl InlayMap {
}
}
- pub fn splice<T: Into<Rope>>(
+ pub fn splice(
&mut self,
to_remove: Vec<InlayId>,
- to_insert: Vec<(InlayId, InlayProperties<T>)>,
+ to_insert: Vec<Inlay>,
) -> (InlaySnapshot, Vec<InlayEdit>) {
let snapshot = &mut self.snapshot;
let mut edits = BTreeSet::new();
- self.inlays.retain(|inlay| !to_remove.contains(&inlay.id));
- for inlay_id in to_remove {
- if let Some(inlay) = self.inlays_by_id.remove(&inlay_id) {
+ self.inlays.retain(|inlay| {
+ let retain = !to_remove.contains(&inlay.id);
+ if !retain {
let offset = inlay.position.to_offset(&snapshot.buffer);
edits.insert(offset);
}
- }
-
- for (existing_id, properties) in to_insert {
- let inlay = Inlay {
- id: existing_id,
- position: properties.position,
- text: properties.text.into(),
- };
+ retain
+ });
+ for inlay_to_insert in to_insert {
// Avoid inserting empty inlays.
- if inlay.text.is_empty() {
+ if inlay_to_insert.text.is_empty() {
continue;
}
- self.inlays_by_id.insert(inlay.id, inlay.clone());
- match self
- .inlays
- .binary_search_by(|probe| probe.position.cmp(&inlay.position, &snapshot.buffer))
- {
+ let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
+ match self.inlays.binary_search_by(|probe| {
+ probe
+ .position
+ .cmp(&inlay_to_insert.position, &snapshot.buffer)
+ }) {
Ok(ix) | Err(ix) => {
- self.inlays.insert(ix, inlay.clone());
+ self.inlays.insert(ix, inlay_to_insert);
}
}
- let offset = inlay.position.to_offset(&snapshot.buffer);
edits.insert(offset);
}
@@ -606,15 +618,19 @@ impl InlayMap {
} else {
InlayId::Suggestion(post_inc(next_inlay_id))
};
- to_insert.push((
- inlay_id,
- InlayProperties {
- position: snapshot.buffer.anchor_at(position, bias),
- text,
- },
- ));
+ to_insert.push(Inlay {
+ id: inlay_id,
+ position: snapshot.buffer.anchor_at(position, bias),
+ text: text.into(),
+ });
} else {
- to_remove.push(*self.inlays_by_id.keys().choose(rng).unwrap());
+ to_remove.push(
+ self.inlays
+ .iter()
+ .choose(rng)
+ .map(|inlay| inlay.id)
+ .unwrap(),
+ );
}
}
log::info!("removing inlays: {:?}", to_remove);
@@ -1095,6 +1111,7 @@ mod tests {
use super::*;
use crate::{InlayId, MultiBuffer};
use gpui::AppContext;
+ use project::{InlayHint, InlayHintLabel};
use rand::prelude::*;
use settings::SettingsStore;
use std::{cmp::Reverse, env, sync::Arc};
@@ -1102,6 +1119,89 @@ mod tests {
use text::Patch;
use util::post_inc;
+ #[test]
+ fn test_inlay_properties_label_padding() {
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String("a".to_string()),
+ buffer_id: 0,
+ position: text::Anchor::default(),
+ padding_left: false,
+ padding_right: false,
+ tooltip: None,
+ kind: None,
+ },
+ )
+ .text
+ .to_string(),
+ "a",
+ "Should not pad label if not requested"
+ );
+
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String("a".to_string()),
+ buffer_id: 0,
+ position: text::Anchor::default(),
+ padding_left: true,
+ padding_right: true,
+ tooltip: None,
+ kind: None,
+ },
+ )
+ .text
+ .to_string(),
+ " a ",
+ "Should pad label for every side requested"
+ );
+
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String(" a ".to_string()),
+ buffer_id: 0,
+ position: text::Anchor::default(),
+ padding_left: false,
+ padding_right: false,
+ tooltip: None,
+ kind: None,
+ },
+ )
+ .text
+ .to_string(),
+ " a ",
+ "Should not change already padded label"
+ );
+
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String(" a ".to_string()),
+ buffer_id: 0,
+ position: text::Anchor::default(),
+ padding_left: true,
+ padding_right: true,
+ tooltip: None,
+ kind: None,
+ },
+ )
+ .text
+ .to_string(),
+ " a ",
+ "Should not change already padded label"
+ );
+ }
+
#[gpui::test]
fn test_basic_inlays(cx: &mut AppContext) {
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
@@ -1112,13 +1212,11 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice(
Vec::new(),
- vec![(
- InlayId::Hint(post_inc(&mut next_inlay_id)),
- InlayProperties {
- position: buffer.read(cx).snapshot(cx).anchor_after(3),
- text: "|123|",
- },
- )],
+ vec![Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|123|".into(),
+ }],
);
assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
assert_eq!(
@@ -1191,20 +1289,16 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice(
Vec::new(),
vec![
- (
- InlayId::Hint(post_inc(&mut next_inlay_id)),
- InlayProperties {
- position: buffer.read(cx).snapshot(cx).anchor_before(3),
- text: "|123|",
- },
- ),
- (
- InlayId::Suggestion(post_inc(&mut next_inlay_id)),
- InlayProperties {
- position: buffer.read(cx).snapshot(cx).anchor_after(3),
- text: "|456|",
- },
- ),
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(3),
+ text: "|123|".into(),
+ },
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|456|".into(),
+ },
],
);
assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
@@ -1389,8 +1483,10 @@ mod tests {
);
// The inlays can be manually removed.
- let (inlay_snapshot, _) = inlay_map
- .splice::<String>(inlay_map.inlays_by_id.keys().copied().collect(), Vec::new());
+ let (inlay_snapshot, _) = inlay_map.splice(
+ inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
+ Vec::new(),
+ );
assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
}
@@ -1404,27 +1500,21 @@ mod tests {
let (inlay_snapshot, _) = inlay_map.splice(
Vec::new(),
vec![
- (
- InlayId::Hint(post_inc(&mut next_inlay_id)),
- InlayProperties {
- position: buffer.read(cx).snapshot(cx).anchor_before(0),
- text: "|123|\n",
- },
- ),
- (
- InlayId::Hint(post_inc(&mut next_inlay_id)),
- InlayProperties {
- position: buffer.read(cx).snapshot(cx).anchor_before(4),
- text: "|456|",
- },
- ),
- (
- InlayId::Suggestion(post_inc(&mut next_inlay_id)),
- InlayProperties {
- position: buffer.read(cx).snapshot(cx).anchor_before(7),
- text: "\n|567|\n",
- },
- ),
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(0),
+ text: "|123|\n".into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(4),
+ text: "|456|".into(),
+ },
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(7),
+ text: "\n|567|\n".into(),
+ },
],
);
assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
@@ -1514,7 +1604,7 @@ mod tests {
(offset, inlay.clone())
})
.collect::<Vec<_>>();
- let mut expected_text = Rope::from(buffer_snapshot.text().as_str());
+ let mut expected_text = Rope::from(buffer_snapshot.text());
for (offset, inlay) in inlays.into_iter().rev() {
expected_text.replace(offset..offset, &inlay.text.to_string());
}
@@ -26,7 +26,7 @@ use aho_corasick::AhoCorasick;
use anyhow::{anyhow, Result};
use blink_manager::BlinkManager;
use client::{ClickhouseEvent, TelemetrySettings};
-use clock::ReplicaId;
+use clock::{Global, ReplicaId};
use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
use copilot::Copilot;
pub use display_map::DisplayPoint;
@@ -190,6 +190,15 @@ pub enum InlayId {
Hint(usize),
}
+impl InlayId {
+ fn id(&self) -> usize {
+ match self {
+ Self::Suggestion(id) => *id,
+ Self::Hint(id) => *id,
+ }
+ }
+}
+
actions!(
editor,
[
@@ -1195,11 +1204,11 @@ enum GotoDefinitionKind {
Type,
}
-#[derive(Debug, Copy, Clone)]
+#[derive(Debug, Clone)]
enum InlayRefreshReason {
SettingsChange(InlayHintSettings),
NewLinesShown,
- ExcerptEdited,
+ BufferEdited(HashSet<Arc<Language>>),
RefreshRequested,
}
@@ -2026,6 +2035,7 @@ impl Editor {
}
let selections = self.selections.all_adjusted(cx);
+ let mut brace_inserted = false;
let mut edits = Vec::new();
let mut new_selections = Vec::with_capacity(selections.len());
let mut new_autoclose_regions = Vec::new();
@@ -2084,6 +2094,7 @@ impl Editor {
selection.range(),
format!("{}{}", text, bracket_pair.end).into(),
));
+ brace_inserted = true;
continue;
}
}
@@ -2110,6 +2121,7 @@ impl Editor {
selection.end..selection.end,
bracket_pair.end.as_str().into(),
));
+ brace_inserted = true;
new_selections.push((
Selection {
id: selection.id,
@@ -2177,8 +2189,7 @@ impl Editor {
let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
- // When buffer contents is updated and caret is moved, try triggering on type formatting.
- if settings::get::<EditorSettings>(cx).use_on_type_format {
+ if !brace_inserted && settings::get::<EditorSettings>(cx).use_on_type_format {
if let Some(on_type_format_task) =
this.trigger_on_type_formatting(text.to_string(), cx)
{
@@ -2617,7 +2628,7 @@ impl Editor {
return;
}
- let invalidate_cache = match reason {
+ let (invalidate_cache, required_languages) = match reason {
InlayRefreshReason::SettingsChange(new_settings) => {
match self.inlay_hint_cache.update_settings(
&self.buffer,
@@ -2633,16 +2644,18 @@ impl Editor {
return;
}
ControlFlow::Break(None) => return,
- ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested,
+ ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
}
}
- InlayRefreshReason::NewLinesShown => InvalidationStrategy::None,
- InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited,
- InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested,
+ InlayRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
+ InlayRefreshReason::BufferEdited(buffer_languages) => {
+ (InvalidationStrategy::BufferEdited, Some(buffer_languages))
+ }
+ InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None),
};
self.inlay_hint_cache.refresh_inlay_hints(
- self.excerpt_visible_offsets(cx),
+ self.excerpt_visible_offsets(required_languages.as_ref(), cx),
invalidate_cache,
cx,
)
@@ -2661,8 +2674,9 @@ impl Editor {
fn excerpt_visible_offsets(
&self,
+ restrict_to_languages: Option<&HashSet<Arc<Language>>>,
cx: &mut ViewContext<'_, '_, Editor>,
- ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> {
+ ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let multi_buffer_visible_start = self
@@ -2680,8 +2694,22 @@ impl Editor {
.range_to_buffer_ranges(multi_buffer_visible_range, cx)
.into_iter()
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
- .map(|(buffer, excerpt_visible_range, excerpt_id)| {
- (excerpt_id, (buffer, excerpt_visible_range))
+ .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
+ let buffer = buffer_handle.read(cx);
+ let language = buffer.language()?;
+ if let Some(restrict_to_languages) = restrict_to_languages {
+ if !restrict_to_languages.contains(language) {
+ return None;
+ }
+ }
+ Some((
+ excerpt_id,
+ (
+ buffer_handle,
+ buffer.version().clone(),
+ excerpt_visible_range,
+ ),
+ ))
})
.collect()
}
@@ -2689,26 +2717,11 @@ impl Editor {
fn splice_inlay_hints(
&self,
to_remove: Vec<InlayId>,
- to_insert: Vec<(Anchor, InlayId, project::InlayHint)>,
+ to_insert: Vec<Inlay>,
cx: &mut ViewContext<Self>,
) {
- let buffer = self.buffer.read(cx).read(cx);
- let new_inlays = to_insert
- .into_iter()
- .map(|(position, id, hint)| {
- let mut text = hint.text();
- if hint.padding_right {
- text.push(' ');
- }
- if hint.padding_left {
- text.insert(0, ' ');
- }
- (id, InlayProperties { position, text })
- })
- .collect();
- drop(buffer);
self.display_map.update(cx, |display_map, cx| {
- display_map.splice_inlays(to_remove, new_inlays, cx);
+ display_map.splice_inlays(to_remove, to_insert, cx);
});
}
@@ -3393,7 +3406,7 @@ impl Editor {
}
self.display_map.update(cx, |map, cx| {
- map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx)
+ map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
});
cx.notify();
true
@@ -3426,7 +3439,7 @@ impl Editor {
fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
let suggestion = self.copilot_state.suggestion.take()?;
self.display_map.update(cx, |map, cx| {
- map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx);
+ map.splice_inlays(vec![suggestion.id], Default::default(), cx);
});
let buffer = self.buffer.read(cx).read(cx);
@@ -3457,21 +3470,11 @@ impl Editor {
to_remove.push(suggestion.id);
}
- let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id));
- let to_insert = vec![(
- suggestion_inlay_id,
- InlayProperties {
- position: cursor,
- text: text.clone(),
- },
- )];
+ let suggestion_inlay =
+ Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
+ self.copilot_state.suggestion = Some(suggestion_inlay.clone());
self.display_map.update(cx, move |map, cx| {
- map.splice_inlays(to_remove, to_insert, cx)
- });
- self.copilot_state.suggestion = Some(Inlay {
- id: suggestion_inlay_id,
- position: cursor,
- text,
+ map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
});
cx.notify();
} else {
@@ -7256,7 +7259,7 @@ impl Editor {
fn on_buffer_event(
&mut self,
- _: ModelHandle<MultiBuffer>,
+ multibuffer: ModelHandle<MultiBuffer>,
event: &multi_buffer::Event,
cx: &mut ViewContext<Self>,
) {
@@ -7268,7 +7271,33 @@ impl Editor {
self.update_visible_copilot_suggestion(cx);
}
cx.emit(Event::BufferEdited);
- self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
+
+ if let Some(project) = &self.project {
+ let project = project.read(cx);
+ let languages_affected = multibuffer
+ .read(cx)
+ .all_buffers()
+ .into_iter()
+ .filter_map(|buffer| {
+ let buffer = buffer.read(cx);
+ let language = buffer.language()?;
+ if project.is_local()
+ && project.language_servers_for_buffer(buffer, cx).count() == 0
+ {
+ None
+ } else {
+ Some(language)
+ }
+ })
+ .cloned()
+ .collect::<HashSet<_>>();
+ if !languages_affected.is_empty() {
+ self.refresh_inlays(
+ InlayRefreshReason::BufferEdited(languages_affected),
+ cx,
+ );
+ }
+ }
}
multi_buffer::Event::ExcerptsAdded {
buffer,
@@ -15,6 +15,7 @@ pub struct EditorSettings {
pub struct Scrollbar {
pub show: ShowScrollbar,
pub git_diff: bool,
+ pub selections: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@@ -39,6 +40,7 @@ pub struct EditorSettingsContent {
pub struct ScrollbarContent {
pub show: Option<ShowScrollbar>,
pub git_diff: Option<bool>,
+ pub selections: Option<bool>,
}
impl Setting for EditorSettings {
@@ -6979,6 +6979,111 @@ async fn test_copilot_disabled_globs(
assert!(copilot_requests.try_next().is_ok());
}
+#[gpui::test]
+async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ brackets: BracketPairConfig {
+ pairs: vec![BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ }],
+ disabled_scopes_by_bracket_ix: Vec::new(),
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+ first_trigger_character: "{".to_string(),
+ more_trigger_character: None,
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { let a = 5; }",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor_handle = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
+ assert_eq!(
+ params.text_document_position.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ assert_eq!(
+ params.text_document_position.position,
+ lsp::Position::new(0, 21),
+ );
+
+ Ok(Some(vec![lsp::TextEdit {
+ new_text: "]".to_string(),
+ range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
+ }]))
+ });
+
+ editor_handle.update(cx, |editor, cx| {
+ cx.focus(&editor_handle);
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
+ });
+ editor.handle_input("{", cx);
+ });
+
+ cx.foreground().run_until_parked();
+
+ buffer.read_with(cx, |buffer, _| {
+ assert_eq!(
+ buffer.text(),
+ "fn main() { let a = {5}; }",
+ "No extra braces from on type formatting should appear in the buffer"
+ )
+ });
+}
+
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
let point = DisplayPoint::new(row as u32, column as u32);
point..point
@@ -1008,6 +1008,7 @@ impl EditorElement {
bounds: RectF,
layout: &mut LayoutState,
cx: &mut ViewContext<Editor>,
+ editor: &Editor,
) {
enum ScrollbarMouseHandlers {}
if layout.mode != EditorMode::Full {
@@ -1050,9 +1051,74 @@ impl EditorElement {
background: style.track.background_color,
..Default::default()
});
+ let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
+ let theme = theme::current(cx);
+ let scrollbar_theme = &theme.editor.scrollbar;
+ if layout.is_singleton && scrollbar_settings.selections {
+ let start_anchor = Anchor::min();
+ let end_anchor = Anchor::max();
+ let mut start_row = None;
+ let mut end_row = None;
+ let color = scrollbar_theme.selections;
+ let border = Border {
+ width: 1.,
+ color: style.thumb.border.color,
+ overlay: false,
+ top: false,
+ right: true,
+ bottom: false,
+ left: true,
+ };
+ let mut push_region = |start, end| {
+ if let (Some(start_display), Some(end_display)) = (start, end) {
+ let start_y = y_for_row(start_display as f32);
+ let mut end_y = y_for_row(end_display as f32);
+ if end_y - start_y < 1. {
+ end_y = start_y + 1.;
+ }
+ let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+ scene.push_quad(Quad {
+ bounds,
+ background: Some(color),
+ border,
+ corner_radius: style.thumb.corner_radius,
+ })
+ }
+ };
+ for (row, _) in &editor.background_highlights_in_range(
+ start_anchor..end_anchor,
+ &layout.position_map.snapshot,
+ &theme,
+ ) {
+ let start_display = row.start;
+ let end_display = row.end;
+
+ if start_row.is_none() {
+ assert_eq!(end_row, None);
+ start_row = Some(start_display.row());
+ end_row = Some(end_display.row());
+ continue;
+ }
+ if let Some(current_end) = end_row.as_mut() {
+ if start_display.row() > *current_end + 1 {
+ push_region(start_row, end_row);
+ start_row = Some(start_display.row());
+ end_row = Some(end_display.row());
+ } else {
+ // Merge two hunks.
+ *current_end = end_display.row();
+ }
+ } else {
+ unreachable!();
+ }
+ }
+ // We might still have a hunk that was not rendered (if there was a search hit on the last line)
+ push_region(start_row, end_row);
+ }
- if layout.is_singleton && settings::get::<EditorSettings>(cx).scrollbar.git_diff {
- let diff_style = theme::current(cx).editor.scrollbar.git.clone();
+ if layout.is_singleton && scrollbar_settings.git_diff {
+ let diff_style = scrollbar_theme.git.clone();
for hunk in layout
.position_map
.snapshot
@@ -2368,7 +2434,7 @@ impl Element<Editor> for EditorElement {
if !layout.blocks.is_empty() {
self.paint_blocks(scene, bounds, visible_bounds, layout, editor, cx);
}
- self.paint_scrollbar(scene, bounds, layout, cx);
+ self.paint_scrollbar(scene, bounds, layout, cx, &editor);
scene.pop_layer();
scene.pop_layer();
@@ -38,14 +38,14 @@ pub struct CachedExcerptHints {
#[derive(Debug, Clone, Copy)]
pub enum InvalidationStrategy {
RefreshRequested,
- ExcerptEdited,
+ BufferEdited,
None,
}
#[derive(Debug, Default)]
pub struct InlaySplice {
pub to_remove: Vec<InlayId>,
- pub to_insert: Vec<(Anchor, InlayId, InlayHint)>,
+ pub to_insert: Vec<Inlay>,
}
struct UpdateTask {
@@ -94,7 +94,7 @@ impl InvalidationStrategy {
fn should_invalidate(&self) -> bool {
matches!(
self,
- InvalidationStrategy::RefreshRequested | InvalidationStrategy::ExcerptEdited
+ InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
)
}
}
@@ -197,7 +197,7 @@ impl InlayHintCache {
pub fn refresh_inlay_hints(
&mut self,
- mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+ mut excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy,
cx: &mut ViewContext<Editor>,
) {
@@ -285,13 +285,13 @@ impl InlayHintCache {
if !old_kinds.contains(&cached_hint.kind)
&& new_kinds.contains(&cached_hint.kind)
{
- to_insert.push((
+ to_insert.push(Inlay::hint(
+ cached_hint_id.id(),
multi_buffer_snapshot.anchor_in_excerpt(
*excerpt_id,
cached_hint.position,
),
- *cached_hint_id,
- cached_hint.clone(),
+ &cached_hint,
));
}
excerpt_cache.next();
@@ -307,11 +307,11 @@ impl InlayHintCache {
for (cached_hint_id, maybe_missed_cached_hint) in excerpt_cache {
let cached_hint_kind = maybe_missed_cached_hint.kind;
if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
- to_insert.push((
+ to_insert.push(Inlay::hint(
+ cached_hint_id.id(),
multi_buffer_snapshot
.anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
- *cached_hint_id,
- maybe_missed_cached_hint.clone(),
+ &maybe_missed_cached_hint,
));
}
}
@@ -342,105 +342,114 @@ impl InlayHintCache {
fn spawn_new_update_tasks(
editor: &mut Editor,
- excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)>,
+ excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
invalidate: InvalidationStrategy,
update_cache_version: usize,
cx: &mut ViewContext<'_, '_, Editor>,
) {
let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
- for (excerpt_id, (buffer_handle, excerpt_visible_range)) in excerpts_to_query {
- if !excerpt_visible_range.is_empty() {
- let buffer = buffer_handle.read(cx);
- let buffer_snapshot = buffer.snapshot();
- let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
- if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
- let new_task_buffer_version = buffer_snapshot.version();
- let cached_excerpt_hints = cached_excerpt_hints.read();
- let cached_buffer_version = &cached_excerpt_hints.buffer_version;
- if cached_excerpt_hints.version > update_cache_version
- || cached_buffer_version.changed_since(new_task_buffer_version)
- {
- return;
- }
- if !new_task_buffer_version.changed_since(&cached_buffer_version)
- && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
- {
- return;
- }
- };
+ for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in
+ excerpts_to_query
+ {
+ if excerpt_visible_range.is_empty() {
+ continue;
+ }
+ let buffer = buffer_handle.read(cx);
+ let buffer_snapshot = buffer.snapshot();
+ if buffer_snapshot
+ .version()
+ .changed_since(&new_task_buffer_version)
+ {
+ continue;
+ }
- let buffer_id = buffer.remote_id();
- let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
- let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
-
- let (multi_buffer_snapshot, full_excerpt_range) =
- editor.buffer.update(cx, |multi_buffer, cx| {
- let multi_buffer_snapshot = multi_buffer.snapshot(cx);
- (
- multi_buffer_snapshot,
- multi_buffer
- .excerpts_for_buffer(&buffer_handle, cx)
- .into_iter()
- .find(|(id, _)| id == &excerpt_id)
- .map(|(_, range)| range.context),
- )
- });
+ let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+ if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ let cached_buffer_version = &cached_excerpt_hints.buffer_version;
+ if cached_excerpt_hints.version > update_cache_version
+ || cached_buffer_version.changed_since(&new_task_buffer_version)
+ {
+ continue;
+ }
+ if !new_task_buffer_version.changed_since(&cached_buffer_version)
+ && !matches!(invalidate, InvalidationStrategy::RefreshRequested)
+ {
+ continue;
+ }
+ };
- if let Some(full_excerpt_range) = full_excerpt_range {
- let query = ExcerptQuery {
- buffer_id,
- excerpt_id,
- dimensions: ExcerptDimensions {
- excerpt_range_start: full_excerpt_range.start,
- excerpt_range_end: full_excerpt_range.end,
- excerpt_visible_range_start,
- excerpt_visible_range_end,
- },
- cache_version: update_cache_version,
- invalidate,
- };
+ let buffer_id = buffer.remote_id();
+ let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start);
+ let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end);
+
+ let (multi_buffer_snapshot, full_excerpt_range) =
+ editor.buffer.update(cx, |multi_buffer, cx| {
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+ (
+ multi_buffer_snapshot,
+ multi_buffer
+ .excerpts_for_buffer(&buffer_handle, cx)
+ .into_iter()
+ .find(|(id, _)| id == &excerpt_id)
+ .map(|(_, range)| range.context),
+ )
+ });
- let new_update_task = |is_refresh_after_regular_task| {
- new_update_task(
- query,
- multi_buffer_snapshot,
- buffer_snapshot,
- Arc::clone(&visible_hints),
- cached_excerpt_hints,
- is_refresh_after_regular_task,
- cx,
- )
- };
- match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
- hash_map::Entry::Occupied(mut o) => {
- let update_task = o.get_mut();
- match (update_task.invalidate, invalidate) {
- (_, InvalidationStrategy::None) => {}
- (
- InvalidationStrategy::ExcerptEdited,
- InvalidationStrategy::RefreshRequested,
- ) if !update_task.task.is_running_rx.is_closed() => {
- update_task.pending_refresh = Some(query);
- }
- _ => {
- o.insert(UpdateTask {
- invalidate,
- cache_version: query.cache_version,
- task: new_update_task(false),
- pending_refresh: None,
- });
- }
+ if let Some(full_excerpt_range) = full_excerpt_range {
+ let query = ExcerptQuery {
+ buffer_id,
+ excerpt_id,
+ dimensions: ExcerptDimensions {
+ excerpt_range_start: full_excerpt_range.start,
+ excerpt_range_end: full_excerpt_range.end,
+ excerpt_visible_range_start,
+ excerpt_visible_range_end,
+ },
+ cache_version: update_cache_version,
+ invalidate,
+ };
+
+ let new_update_task = |is_refresh_after_regular_task| {
+ new_update_task(
+ query,
+ multi_buffer_snapshot,
+ buffer_snapshot,
+ Arc::clone(&visible_hints),
+ cached_excerpt_hints,
+ is_refresh_after_regular_task,
+ cx,
+ )
+ };
+ match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+ hash_map::Entry::Occupied(mut o) => {
+ let update_task = o.get_mut();
+ match (update_task.invalidate, invalidate) {
+ (_, InvalidationStrategy::None) => {}
+ (
+ InvalidationStrategy::BufferEdited,
+ InvalidationStrategy::RefreshRequested,
+ ) if !update_task.task.is_running_rx.is_closed() => {
+ update_task.pending_refresh = Some(query);
+ }
+ _ => {
+ o.insert(UpdateTask {
+ invalidate,
+ cache_version: query.cache_version,
+ task: new_update_task(false),
+ pending_refresh: None,
+ });
}
- }
- hash_map::Entry::Vacant(v) => {
- v.insert(UpdateTask {
- invalidate,
- cache_version: query.cache_version,
- task: new_update_task(false),
- pending_refresh: None,
- });
}
}
+ hash_map::Entry::Vacant(v) => {
+ v.insert(UpdateTask {
+ invalidate,
+ cache_version: query.cache_version,
+ task: new_update_task(false),
+ pending_refresh: None,
+ });
+ }
}
}
}
@@ -648,18 +657,22 @@ async fn fetch_and_update_hints(
for new_hint in new_update.add_to_cache {
let new_hint_position = multi_buffer_snapshot
.anchor_in_excerpt(query.excerpt_id, new_hint.position);
- let new_inlay_id = InlayId::Hint(post_inc(&mut editor.next_inlay_id));
+ let new_inlay_id = post_inc(&mut editor.next_inlay_id);
if editor
.inlay_hint_cache
.allowed_hint_kinds
.contains(&new_hint.kind)
{
- splice
- .to_insert
- .push((new_hint_position, new_inlay_id, new_hint.clone()));
+ splice.to_insert.push(Inlay::hint(
+ new_inlay_id,
+ new_hint_position,
+ &new_hint,
+ ));
}
- cached_excerpt_hints.hints.push((new_inlay_id, new_hint));
+ cached_excerpt_hints
+ .hints
+ .push((InlayId::Hint(new_inlay_id), new_hint));
}
cached_excerpt_hints
@@ -820,7 +833,7 @@ mod tests {
use crate::{
scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
serde_json::json,
- ExcerptRange, InlayHintSettings,
+ ExcerptRange,
};
use futures::StreamExt;
use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
@@ -961,6 +974,348 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ let current_call_id =
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, current_call_id),
+ label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ let progress_token = "test_progress_token";
+ fake_server
+ .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
+ token: lsp::ProgressToken::String(progress_token.to_string()),
+ })
+ .await
+ .expect("work done progress create request failed");
+ cx.foreground().run_until_parked();
+ fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+ token: lsp::ProgressToken::String(progress_token.to_string()),
+ value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
+ lsp::WorkDoneProgressBegin::default(),
+ )),
+ });
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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,
+ "Should not update the cache while the work task is running"
+ );
+ });
+
+ fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+ token: lsp::ProgressToken::String(progress_token.to_string()),
+ value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
+ lsp::WorkDoneProgressEnd::default(),
+ )),
+ });
+ cx.foreground().run_until_parked();
+
+ edits_made += 1;
+ editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["1".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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,
+ "Cache version should udpate once after the work task is done"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.md": "Test md file with some text",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ 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 mut rs_fake_servers = None;
+ let mut md_fake_servers = None;
+ for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
+ let mut language = Language::new(
+ LanguageConfig {
+ name: name.into(),
+ path_suffixes: vec![path_suffix.to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ name,
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ match name {
+ "Rust" => rs_fake_servers = Some(fake_servers),
+ "Markdown" => md_fake_servers = Some(fake_servers),
+ _ => unreachable!(),
+ }
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::new(language));
+ });
+ }
+
+ let _rs_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
+ let rs_editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
+ rs_fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ rs_editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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,
+ "Rust editor update the cache version after every cache/view change"
+ );
+ });
+
+ cx.foreground().run_until_parked();
+ let _md_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/other.md", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
+ let md_editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "other.md"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let md_lsp_request_count = Arc::new(AtomicU32::new(0));
+ md_fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/other.md").unwrap(),
+ );
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ md_editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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);
+ });
+
+ rs_editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some rs change", cx);
+ });
+ cx.foreground().run_until_parked();
+ rs_editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["1".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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,
+ "Every time hint cache changes, cache version should be incremented"
+ );
+ });
+ md_editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["0".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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);
+ });
+
+ md_editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some md change", cx);
+ });
+ cx.foreground().run_until_parked();
+ md_editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["1".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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);
+ });
+ rs_editor.update(cx, |editor, cx| {
+ let expected_layers = vec!["1".to_string()];
+ assert_eq!(
+ expected_layers,
+ cached_hint_labels(editor),
+ "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);
+ });
+ }
+
#[gpui::test]
async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
@@ -1079,7 +1434,6 @@ mod tests {
visible_hint_labels(editor, cx)
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(
inlay_cache.version, edits_made,
"Should not update cache version due to new loaded hints being the same"
@@ -1215,7 +1569,6 @@ 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.allowed_hint_kinds, another_allowed_hint_kinds);
assert_eq!(
inlay_cache.version, edits_made,
"The editor should not update the cache version after /refresh query without updates"
@@ -1289,20 +1642,18 @@ mod tests {
visible_hint_labels(editor, cx),
);
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds);
assert_eq!(inlay_cache.version, edits_made);
});
}
#[gpui::test]
async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
- let allowed_hint_kinds = HashSet::from_iter([None]);
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
- show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
- show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
- show_other_hints: allowed_hint_kinds.contains(&None),
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
})
});
@@ -1370,7 +1721,6 @@ mod tests {
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(
inlay_cache.version, 1,
"Only one update should be registered in the cache after all cancellations"
@@ -1417,7 +1767,6 @@ mod tests {
);
assert_eq!(expected_hints, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(
inlay_cache.version, 2,
"Should update the cache version once more, for the new change"
@@ -1427,13 +1776,12 @@ mod tests {
#[gpui::test]
async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
- let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
- show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
- show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
- show_other_hints: allowed_hint_kinds.contains(&None),
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
})
});
@@ -1539,7 +1887,6 @@ mod tests {
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(
inlay_cache.version, 2,
"Both LSP queries should've bumped the cache version"
@@ -1572,7 +1919,6 @@ mod tests {
"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.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added");
});
}
@@ -1582,13 +1928,12 @@ mod tests {
deterministic: Arc<Deterministic>,
cx: &mut gpui::TestAppContext,
) {
- let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
init_test(cx, |settings| {
settings.defaults.inlay_hints = Some(InlayHintSettings {
enabled: true,
- show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
- show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
- show_other_hints: allowed_hint_kinds.contains(&None),
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
})
});
@@ -1794,7 +2139,6 @@ mod tests {
);
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison");
});
@@ -1826,7 +2170,6 @@ mod tests {
"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.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 9);
});
@@ -1855,7 +2198,6 @@ mod tests {
"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.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 12);
});
@@ -1884,7 +2226,6 @@ mod tests {
"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.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer");
});
@@ -1911,7 +2252,6 @@ mod tests {
unedited (2nd) buffer should have the same hint");
assert_eq!(expected_layers, visible_hint_labels(editor, cx));
let inlay_cache = editor.inlay_hint_cache();
- assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
assert_eq!(inlay_cache.version, 16);
});
}
@@ -263,13 +263,13 @@ pub fn find_preceding_boundary(
if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) {
- return prev_point;
+ return map.clip_point(prev_point, Bias::Left);
}
}
prev = Some((ch, point));
}
- DisplayPoint::zero()
+ map.clip_point(DisplayPoint::zero(), Bias::Left)
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found, indicated by the
@@ -292,7 +292,7 @@ pub fn find_preceding_boundary_in_line(
for (ch, point) in map.reverse_chars_at(from) {
if let Some((prev_ch, prev_point)) = prev {
if is_boundary(ch, prev_ch) {
- return prev_point;
+ return map.clip_point(prev_point, Bias::Left);
}
}
@@ -303,7 +303,7 @@ pub fn find_preceding_boundary_in_line(
prev = Some((ch, point));
}
- prev.map(|(_, point)| point).unwrap_or(from)
+ map.clip_point(prev.map(|(_, point)| point).unwrap_or(from), Bias::Left)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
@@ -406,8 +406,12 @@ pub fn split_display_range_by_lines(
#[cfg(test)]
mod tests {
use super::*;
- use crate::{test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange, MultiBuffer};
+ use crate::{
+ display_map::Inlay, test::marked_display_snapshot, Buffer, DisplayMap, ExcerptRange,
+ InlayId, MultiBuffer,
+ };
use settings::SettingsStore;
+ use util::post_inc;
#[gpui::test]
fn test_previous_word_start(cx: &mut gpui::AppContext) {
@@ -505,6 +509,80 @@ mod tests {
});
}
+ #[gpui::test]
+ fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ let input_text = "abcdefghijklmnopqrstuvwxys";
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let buffer = MultiBuffer::build_simple(input_text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let display_map =
+ cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+
+ // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
+ let mut id = 0;
+ let inlays = (0..buffer_snapshot.len())
+ .map(|offset| {
+ [
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Left),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Right),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Left),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Right),
+ text: format!("test").into(),
+ },
+ ]
+ })
+ .flatten()
+ .collect();
+ let snapshot = display_map.update(cx, |map, cx| {
+ map.splice_inlays(Vec::new(), inlays, cx);
+ map.snapshot(cx)
+ });
+
+ assert_eq!(
+ find_preceding_boundary(
+ &snapshot,
+ buffer_snapshot.len().to_display_point(&snapshot),
+ |left, _| left == 'a',
+ ),
+ 0.to_display_point(&snapshot),
+ "Should not stop at inlays when looking for boundaries"
+ );
+
+ assert_eq!(
+ find_preceding_boundary_in_line(
+ &snapshot,
+ buffer_snapshot.len().to_display_point(&snapshot),
+ |left, _| left == 'a',
+ ),
+ 0.to_display_point(&snapshot),
+ "Should not stop at inlays when looking for boundaries in line"
+ );
+ }
+
#[gpui::test]
fn test_next_word_end(cx: &mut gpui::AppContext) {
init_test(cx);
@@ -31,6 +31,7 @@ serde_derive.workspace = true
serde_json.workspace = true
log.workspace = true
libc = "0.2"
+time.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
@@ -279,6 +279,9 @@ impl Fs for RealFs {
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
let buffer_size = text.summary().len.min(10 * 1024);
+ if let Some(path) = path.parent() {
+ self.create_dir(path).await?;
+ }
let file = smol::fs::File::create(path).await?;
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
for chunk in chunks(text, line_ending) {
@@ -1077,6 +1080,9 @@ impl Fs for FakeFs {
self.simulate_random_delay().await;
let path = normalize_path(path);
let content = chunks(text, line_ending).collect();
+ if let Some(path) = path.parent() {
+ self.create_dir(path).await?;
+ }
self.write_file_internal(path, content)?;
Ok(())
}
@@ -1,6 +1,6 @@
use anyhow::Result;
use collections::HashMap;
-use git2::ErrorCode;
+use git2::{BranchType, ErrorCode};
use parking_lot::Mutex;
use rpc::proto;
use serde_derive::{Deserialize, Serialize};
@@ -16,6 +16,12 @@ use util::ResultExt;
pub use git2::Repository as LibGitRepository;
+#[derive(Clone, Debug, Hash, PartialEq)]
+pub struct Branch {
+ pub name: Box<str>,
+ /// Timestamp of most recent commit, normalized to Unix Epoch format.
+ pub unix_timestamp: Option<i64>,
+}
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
@@ -27,6 +33,12 @@ pub trait GitRepository: Send {
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
+ fn branches(&self) -> Result<Vec<Branch>> {
+ Ok(vec![])
+ }
+ fn change_branch(&self, _: &str) -> Result<()> {
+ Ok(())
+ }
}
impl std::fmt::Debug for dyn GitRepository {
@@ -106,6 +118,40 @@ impl GitRepository for LibGitRepository {
}
}
}
+ fn branches(&self) -> Result<Vec<Branch>> {
+ let local_branches = self.branches(Some(BranchType::Local))?;
+ let valid_branches = local_branches
+ .filter_map(|branch| {
+ branch.ok().and_then(|(branch, _)| {
+ let name = branch.name().ok().flatten().map(Box::from)?;
+ let timestamp = branch.get().peel_to_commit().ok()?.time();
+ let unix_timestamp = timestamp.seconds();
+ let timezone_offset = timestamp.offset_minutes();
+ let utc_offset =
+ time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
+ let unix_timestamp =
+ time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
+ Some(Branch {
+ name,
+ unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
+ })
+ })
+ })
+ .collect();
+ Ok(valid_branches)
+ }
+ fn change_branch(&self, name: &str) -> Result<()> {
+ let revision = self.find_branch(name, BranchType::Local)?;
+ let revision = revision.get();
+ let as_tree = revision.peel_to_tree()?;
+ self.checkout_tree(as_tree.as_object(), None)?;
+ self.set_head(
+ revision
+ .name()
+ .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
+ )?;
+ Ok(())
+ }
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
@@ -24,6 +24,7 @@ pub struct GoToLine {
prev_scroll_position: Option<Vector2F>,
cursor_point: Point,
max_point: Point,
+ has_focus: bool,
}
pub enum Event {
@@ -57,6 +58,7 @@ impl GoToLine {
prev_scroll_position: scroll_position,
cursor_point,
max_point,
+ has_focus: false,
}
}
@@ -178,11 +180,20 @@ impl View for GoToLine {
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ self.has_focus = true;
cx.focus(&self.line_editor);
}
+
+ fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+ self.has_focus = false;
+ }
}
impl Modal for GoToLine {
+ fn has_focus(&self) -> bool {
+ self.has_focus
+ }
+
fn dismiss_on_event(event: &Self::Event) -> bool {
matches!(event, Event::Dismissed)
}
@@ -2971,14 +2971,12 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
}
pub fn focus(&mut self, handle: &AnyViewHandle) {
- self.window_context
- .focus(handle.window_id, Some(handle.view_id));
+ self.window_context.focus(Some(handle.view_id));
}
pub fn focus_self(&mut self) {
- let window_id = self.window_id;
let view_id = self.view_id;
- self.window_context.focus(window_id, Some(view_id));
+ self.window_context.focus(Some(view_id));
}
pub fn is_self_focused(&self) -> bool {
@@ -2997,8 +2995,7 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
}
pub fn blur(&mut self) {
- let window_id = self.window_id;
- self.window_context.focus(window_id, None);
+ self.window_context.focus(None);
}
pub fn on_window_should_close<F>(&mut self, mut callback: F)
@@ -3304,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
MouseState {
hovered: self.window.hovered_region_ids.contains(®ion_id),
- clicked: self
- .window
- .clicked_region_ids
- .get(®ion_id)
- .and_then(|_| self.window.clicked_button),
+ clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
+ if region_id == clicked_region_id {
+ Some(button)
+ } else {
+ None
+ }
+ } else {
+ None
+ },
accessed_hovered: false,
accessed_clicked: false,
}
@@ -14,8 +14,8 @@ use crate::{
text_layout::TextLayoutCache,
util::post_inc,
Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect,
- Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription,
- View, ViewContext, ViewHandle, WindowInvalidation,
+ Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, NoAction, SceneBuilder,
+ Subscription, View, ViewContext, ViewHandle, WindowInvalidation,
};
use anyhow::{anyhow, bail, Result};
use collections::{HashMap, HashSet};
@@ -53,7 +53,7 @@ pub struct Window {
last_mouse_moved_event: Option<Event>,
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
- pub(crate) clicked_button: Option<MouseButton>,
+ pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
mouse_position: Vector2F,
text_layout_cache: TextLayoutCache,
}
@@ -86,7 +86,7 @@ impl Window {
last_mouse_moved_event: None,
hovered_region_ids: Default::default(),
clicked_region_ids: Default::default(),
- clicked_button: None,
+ clicked_region: None,
mouse_position: vec2f(0., 0.),
titlebar_height,
appearance,
@@ -434,7 +434,11 @@ impl<'a> WindowContext<'a> {
MatchResult::None => false,
MatchResult::Pending => true,
MatchResult::Matches(matches) => {
+ let no_action_id = (NoAction {}).id();
for (view_id, action) in matches {
+ if action.id() == no_action_id {
+ return false;
+ }
if self.dispatch_action(Some(*view_id), action.as_ref()) {
self.keystroke_matcher.clear_pending();
handled_by = Some(action.boxed_clone());
@@ -480,8 +484,8 @@ impl<'a> WindowContext<'a> {
// specific ancestor element that contained both [positions]'
// So we need to store the overlapping regions on mouse down.
- // If there is already clicked_button stored, don't replace it.
- if self.window.clicked_button.is_none() {
+ // If there is already region being clicked, don't replace it.
+ if self.window.clicked_region.is_none() {
self.window.clicked_region_ids = self
.window
.mouse_regions
@@ -495,7 +499,17 @@ impl<'a> WindowContext<'a> {
})
.collect();
- self.window.clicked_button = Some(e.button);
+ let mut highest_z_index = 0;
+ let mut clicked_region_id = None;
+ for (region, z_index) in self.window.mouse_regions.iter() {
+ if region.bounds.contains_point(e.position) && *z_index >= highest_z_index {
+ highest_z_index = *z_index;
+ clicked_region_id = Some(region.id());
+ }
+ }
+
+ self.window.clicked_region =
+ clicked_region_id.map(|region_id| (region_id, e.button));
}
mouse_events.push(MouseEvent::Down(MouseDown {
@@ -560,7 +574,7 @@ impl<'a> WindowContext<'a> {
prev_mouse_position: self.window.mouse_position,
platform_event: e.clone(),
}));
- } else if let Some(clicked_button) = self.window.clicked_button {
+ } else if let Some((_, clicked_button)) = self.window.clicked_region {
// Mouse up event happened outside the current window. Simulate mouse up button event
let button_event = e.to_button_event(clicked_button);
mouse_events.push(MouseEvent::Up(MouseUp {
@@ -683,8 +697,8 @@ impl<'a> WindowContext<'a> {
// Only raise click events if the released button is the same as the one stored
if self
.window
- .clicked_button
- .map(|clicked_button| clicked_button == e.button)
+ .clicked_region
+ .map(|(_, clicked_button)| clicked_button == e.button)
.unwrap_or(false)
{
// Clear clicked regions and clicked button
@@ -692,7 +706,7 @@ impl<'a> WindowContext<'a> {
&mut self.window.clicked_region_ids,
Default::default(),
);
- self.window.clicked_button = None;
+ self.window.clicked_region = None;
// Find regions which still overlap with the mouse since the last MouseDown happened
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
@@ -867,18 +881,10 @@ impl<'a> WindowContext<'a> {
}
for view_id in &invalidation.updated {
let titlebar_height = self.window.titlebar_height;
- let hovered_region_ids = self.window.hovered_region_ids.clone();
- let clicked_region_ids = self
- .window
- .clicked_button
- .map(|button| (self.window.clicked_region_ids.clone(), button));
-
let element = self
.render_view(RenderParams {
view_id: *view_id,
titlebar_height,
- hovered_region_ids,
- clicked_region_ids,
refreshing: false,
appearance,
})
@@ -1092,6 +1098,10 @@ impl<'a> WindowContext<'a> {
self.window.focused_view_id
}
+ pub fn focus(&mut self, view_id: Option<usize>) {
+ self.app_context.focus(self.window_id, view_id);
+ }
+
pub fn window_bounds(&self) -> WindowBounds {
self.window.platform_window.bounds()
}
@@ -1183,8 +1193,6 @@ impl<'a> WindowContext<'a> {
pub struct RenderParams {
pub view_id: usize,
pub titlebar_height: f32,
- pub hovered_region_ids: HashSet<MouseRegionId>,
- pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
pub refreshing: bool,
pub appearance: Appearance,
}
@@ -31,3 +31,5 @@ pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext};
pub use anyhow;
pub use serde_json;
+
+actions!(zed, [NoAction]);
@@ -4,7 +4,7 @@ use pathfinder_geometry::vector::vec2f;
use crate::{geometry::vector::Vector2F, keymap_matcher::Keystroke};
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KeyDownEvent {
pub keystroke: Keystroke,
pub is_held: bool,
@@ -232,10 +232,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
sel!(canBecomeKeyWindow),
yes as extern "C" fn(&Object, Sel) -> BOOL,
);
- decl.add_method(
- sel!(sendEvent:),
- send_event as extern "C" fn(&Object, Sel, id),
- );
decl.add_method(
sel!(windowDidResize:),
window_did_resize as extern "C" fn(&Object, Sel, id),
@@ -299,7 +295,7 @@ struct WindowState {
appearance_changed_callback: Option<Box<dyn FnMut()>>,
input_handler: Option<Box<dyn InputHandler>>,
pending_key_down: Option<(KeyDownEvent, Option<InsertText>)>,
- performed_key_equivalent: bool,
+ last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize,
executor: Rc<executor::Foreground>,
scene_to_render: Option<Scene>,
@@ -521,7 +517,7 @@ impl Window {
appearance_changed_callback: None,
input_handler: None,
pending_key_down: None,
- performed_key_equivalent: false,
+ last_key_equivalent: None,
synthetic_drag_counter: 0,
executor,
scene_to_render: Default::default(),
@@ -965,36 +961,34 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
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(Event::KeyDown(event)) = event {
+ // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
+ // If that event isn't handled, it will then dispatch a "key down" event. GPUI
+ // makes no distinction between these two types of events, so we need to ignore
+ // the "key down" event if we've already just processed its "key equivalent" version.
if key_equivalent {
- window_state_borrow.performed_key_equivalent = true;
- } else if window_state_borrow.performed_key_equivalent {
+ window_state_borrow.last_key_equivalent = Some(event.clone());
+ } else if window_state_borrow.last_key_equivalent.take().as_ref() == Some(&event) {
return NO;
}
- let function_is_held;
- window_state_borrow.pending_key_down = match event {
- Event::KeyDown(event) => {
- let keydown = event.keystroke.clone();
- // Ignore events from held-down keys after some of the initially-pressed keys
- // were released.
- if event.is_held {
- if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
- return YES;
- }
- } else {
- window_state_borrow.last_fresh_keydown = Some(keydown);
- }
- function_is_held = event.keystroke.function;
- Some((event, None))
+ let keydown = event.keystroke.clone();
+ let fn_modifier = keydown.function;
+ // Ignore events from held-down keys after some of the initially-pressed keys
+ // were released.
+ if event.is_held {
+ if window_state_borrow.last_fresh_keydown.as_ref() != Some(&keydown) {
+ return YES;
}
-
- _ => return NO,
- };
-
+ } else {
+ window_state_borrow.last_fresh_keydown = Some(keydown);
+ }
+ window_state_borrow.pending_key_down = Some((event, None));
drop(window_state_borrow);
- if !function_is_held {
+ // Send the event to the input context for IME handling, unless the `fn` modifier is
+ // being pressed.
+ if !fn_modifier {
unsafe {
let input_context: id = msg_send![this, inputContext];
let _: BOOL = msg_send![input_context, handleEvent: native_event];
@@ -1143,13 +1137,6 @@ extern "C" fn cancel_operation(this: &Object, _sel: Sel, _sender: id) {
}
}
-extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
- unsafe {
- let _: () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
- get_window_state(this).borrow_mut().performed_key_equivalent = false;
- }
-}
-
extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
let window_state = unsafe { get_window_state(this) };
window_state.as_ref().borrow().move_traffic_light();
@@ -4,7 +4,6 @@ mod syntax_map_tests;
use crate::{Grammar, InjectionConfig, Language, LanguageRegistry};
use collections::HashMap;
use futures::FutureExt;
-use lazy_static::lazy_static;
use parking_lot::Mutex;
use std::{
borrow::Cow,
@@ -25,9 +24,7 @@ thread_local! {
static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
}
-lazy_static! {
- static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
-}
+static QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Mutex::new(vec![]);
#[derive(Default)]
pub struct SyntaxMap {
@@ -17,7 +17,6 @@ test-support = [
"async-trait",
"collections/test-support",
"gpui/test-support",
- "lazy_static",
"live_kit_server",
"nanoid",
]
@@ -38,7 +37,6 @@ parking_lot.workspace = true
postage.workspace = true
async-trait = { workspace = true, optional = true }
-lazy_static = { workspace = true, optional = true }
nanoid = { version ="0.4", optional = true}
[dev-dependencies]
@@ -60,7 +58,6 @@ foreign-types = "0.3"
futures.workspace = true
hmac = "0.12"
jwt = "0.16"
-lazy_static.workspace = true
objc = "0.2"
parking_lot.workspace = true
serde.workspace = true
@@ -1,18 +1,15 @@
use anyhow::{anyhow, Result};
use async_trait::async_trait;
-use collections::HashMap;
+use collections::{BTreeMap, HashMap};
use futures::Stream;
use gpui::executor::Background;
-use lazy_static::lazy_static;
use live_kit_server::token;
use media::core_video::CVImageBuffer;
use parking_lot::Mutex;
use postage::watch;
use std::{future::Future, mem, sync::Arc};
-lazy_static! {
- static ref SERVERS: Mutex<HashMap<String, Arc<TestServer>>> = Default::default();
-}
+static SERVERS: Mutex<BTreeMap<String, Arc<TestServer>>> = Mutex::new(BTreeMap::new());
pub struct TestServer {
pub url: String,
@@ -25,6 +25,7 @@ pub struct Picker<D: PickerDelegate> {
theme: Arc<Mutex<Box<dyn Fn(&theme::Theme) -> theme::Picker>>>,
confirmed: bool,
pending_update_matches: Task<Option<()>>,
+ has_focus: bool,
}
pub trait PickerDelegate: Sized + 'static {
@@ -45,10 +46,16 @@ pub trait PickerDelegate: Sized + 'static {
fn center_selection_after_match_updates(&self) -> bool {
false
}
- fn render_header(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
+ fn render_header(
+ &self,
+ _cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<AnyElement<Picker<Self>>> {
None
}
- fn render_footer(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
+ fn render_footer(
+ &self,
+ _cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<AnyElement<Picker<Self>>> {
None
}
}
@@ -140,13 +147,22 @@ impl<D: PickerDelegate> View for Picker<D> {
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ self.has_focus = true;
if cx.is_self_focused() {
cx.focus(&self.query_editor);
}
}
+
+ fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext<Self>) {
+ self.has_focus = false;
+ }
}
impl<D: PickerDelegate> Modal for Picker<D> {
+ fn has_focus(&self) -> bool {
+ self.has_focus
+ }
+
fn dismiss_on_event(event: &Self::Event) -> bool {
matches!(event, PickerEvent::Dismiss)
}
@@ -191,6 +207,7 @@ impl<D: PickerDelegate> Picker<D> {
theme,
confirmed: false,
pending_update_matches: Task::ready(None),
+ has_focus: false,
};
this.update_matches(String::new(), cx);
this
@@ -64,7 +64,7 @@ itertools = "0.10"
[dev-dependencies]
ctor.workspace = true
env_logger.workspace = true
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
db = { path = "../db", features = ["test-support"] }
@@ -1822,11 +1822,21 @@ impl LspCommand for InlayHints {
async fn response_from_lsp(
self,
message: Option<Vec<lsp::InlayHint>>,
- _: ModelHandle<Project>,
+ project: ModelHandle<Project>,
buffer: ModelHandle<Buffer>,
- _: LanguageServerId,
- cx: AsyncAppContext,
+ server_id: LanguageServerId,
+ mut cx: AsyncAppContext,
) -> Result<Vec<InlayHint>> {
+ let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?;
+ // `typescript-language-server` adds padding to the left for type hints, turning
+ // `const foo: boolean` into `const foo : boolean` which looks odd.
+ // `rust-analyzer` does not have the padding for this case, and we have to accomodate both.
+ //
+ // We could trim the whole string, but being pessimistic on par with the situation above,
+ // there might be a hint with multiple whitespaces at the end(s) which we need to display properly.
+ // Hence let's use a heuristic first to handle the most awkward case and look for more.
+ let force_no_type_left_padding =
+ lsp_adapter.name.0.as_ref() == "typescript-language-server";
cx.read(|cx| {
let origin_buffer = buffer.read(cx);
Ok(message
@@ -1840,6 +1850,12 @@ impl LspCommand for InlayHints {
});
let position = origin_buffer
.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
+ let padding_left =
+ if force_no_type_left_padding && kind == Some(InlayHintKind::Type) {
+ false
+ } else {
+ lsp_hint.padding_left.unwrap_or(false)
+ };
InlayHint {
buffer_id: origin_buffer.remote_id(),
position: if kind == Some(InlayHintKind::Parameter) {
@@ -1847,7 +1863,7 @@ impl LspCommand for InlayHints {
} else {
origin_buffer.anchor_after(position)
},
- padding_left: lsp_hint.padding_left.unwrap_or(false),
+ padding_left,
padding_right: lsp_hint.padding_right.unwrap_or(false),
label: match lsp_hint.label {
lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
@@ -778,20 +778,32 @@ impl Project {
}
let mut language_servers_to_stop = Vec::new();
+ let mut language_servers_to_restart = Vec::new();
let languages = self.languages.to_vec();
+ let project_settings = settings::get::<ProjectSettings>(cx).clone();
for (worktree_id, started_lsp_name) in self.language_server_ids.keys() {
- let language = languages.iter().find(|l| {
- l.lsp_adapters()
+ let language = languages.iter().find_map(|l| {
+ let adapter = l
+ .lsp_adapters()
.iter()
- .any(|adapter| &adapter.name == started_lsp_name)
+ .find(|adapter| &adapter.name == started_lsp_name)?;
+ Some((l, adapter))
});
- if let Some(language) = language {
+ if let Some((language, adapter)) = language {
let worktree = self.worktree_for_id(*worktree_id, cx);
- let file = worktree.and_then(|tree| {
+ let file = worktree.as_ref().and_then(|tree| {
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _))
});
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server {
language_servers_to_stop.push((*worktree_id, started_lsp_name.clone()));
+ } else if let Some(worktree) = worktree {
+ let new_lsp_settings = project_settings
+ .lsp
+ .get(&adapter.name.0)
+ .and_then(|s| s.initialization_options.as_ref());
+ if adapter.initialization_options.as_ref() != new_lsp_settings {
+ language_servers_to_restart.push((worktree, Arc::clone(language)));
+ }
}
}
}
@@ -808,6 +820,11 @@ impl Project {
self.start_language_servers(&worktree, worktree_path, language, cx);
}
+ // Restart all language servers with changed initialization options.
+ for (worktree, language) in language_servers_to_restart {
+ self.restart_language_servers(worktree, language, cx);
+ }
+
if !self.copilot_enabled && Copilot::global(cx).is_some() {
self.copilot_enabled = true;
for buffer in self.opened_buffers.values() {
@@ -3398,6 +3415,7 @@ impl Project {
cx: &mut ModelContext<Self>,
) {
if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) {
+ cx.emit(Event::RefreshInlays);
status.pending_work.remove(&token);
cx.notify();
}
@@ -981,6 +981,19 @@ impl LocalWorktree {
})
}
+ /// Find the lowest path in the worktree's datastructures that is an ancestor
+ fn lowest_ancestor(&self, path: &Path) -> PathBuf {
+ let mut lowest_ancestor = None;
+ for path in path.ancestors() {
+ if self.entry_for_path(path).is_some() {
+ lowest_ancestor = Some(path.to_path_buf());
+ break;
+ }
+ }
+
+ lowest_ancestor.unwrap_or_else(|| PathBuf::from(""))
+ }
+
pub fn create_entry(
&self,
path: impl Into<Arc<Path>>,
@@ -988,6 +1001,7 @@ impl LocalWorktree {
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
let path = path.into();
+ let lowest_ancestor = self.lowest_ancestor(&path);
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
let write = cx.background().spawn(async move {
@@ -1001,10 +1015,31 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
write.await?;
- this.update(&mut cx, |this, cx| {
- this.as_local_mut().unwrap().refresh_entry(path, None, cx)
- })
- .await
+ let (result, refreshes) = this.update(&mut cx, |this, cx| {
+ let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+ let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
+ for refresh_path in refresh_paths.ancestors() {
+ if refresh_path == Path::new("") {
+ continue;
+ }
+ let refresh_full_path = lowest_ancestor.join(refresh_path);
+
+ refreshes.push(this.as_local_mut().unwrap().refresh_entry(
+ refresh_full_path.into(),
+ None,
+ cx,
+ ));
+ }
+ (
+ this.as_local_mut().unwrap().refresh_entry(path, None, cx),
+ refreshes,
+ )
+ });
+ for refresh in refreshes {
+ refresh.await.log_err();
+ }
+
+ result.await
})
}
@@ -2140,6 +2175,7 @@ impl LocalSnapshot {
impl BackgroundScannerState {
fn should_scan_directory(&self, entry: &Entry) -> bool {
(!entry.is_external && !entry.is_ignored)
+ || entry.path.file_name() == Some(&*DOT_GIT)
|| self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
|| self
.paths_to_scan
@@ -2319,6 +2355,7 @@ impl BackgroundScannerState {
.entry_for_id(entry_id)
.map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue };
+ log::info!("reload git repository {:?}", dot_git_dir);
let repository = repository.repo_ptr.lock();
let branch = repository.branch_name();
repository.reload_index();
@@ -2359,6 +2396,8 @@ impl BackgroundScannerState {
}
fn build_repository(&mut self, dot_git_path: Arc<Path>, fs: &dyn Fs) -> Option<()> {
+ log::info!("build git repository {:?}", dot_git_path);
+
let work_dir_path: Arc<Path> = dot_git_path.parent().unwrap().into();
// Guard against repositories inside the repository metadata
@@ -3138,8 +3177,6 @@ impl BackgroundScanner {
}
async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
- log::debug!("received fs events {:?}", abs_paths);
-
let root_path = self.state.lock().snapshot.abs_path.clone();
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
Ok(path) => path,
@@ -3150,7 +3187,6 @@ impl BackgroundScanner {
};
let mut relative_paths = Vec::with_capacity(abs_paths.len());
- let mut unloaded_relative_paths = Vec::new();
abs_paths.sort_unstable();
abs_paths.dedup_by(|a, b| a.starts_with(&b));
abs_paths.retain(|abs_path| {
@@ -3173,7 +3209,6 @@ impl BackgroundScanner {
});
if !parent_dir_is_loaded {
log::debug!("ignoring event {relative_path:?} within unloaded directory");
- unloaded_relative_paths.push(relative_path);
return false;
}
@@ -3182,27 +3217,30 @@ impl BackgroundScanner {
}
});
- if !relative_paths.is_empty() {
- let (scan_job_tx, scan_job_rx) = channel::unbounded();
- self.reload_entries_for_paths(
- root_path,
- root_canonical_path,
- &relative_paths,
- abs_paths,
- Some(scan_job_tx.clone()),
- )
- .await;
- drop(scan_job_tx);
- self.scan_dirs(false, scan_job_rx).await;
-
- let (scan_job_tx, scan_job_rx) = channel::unbounded();
- self.update_ignore_statuses(scan_job_tx).await;
- self.scan_dirs(false, scan_job_rx).await;
+ if relative_paths.is_empty() {
+ return;
}
+ log::debug!("received fs events {:?}", relative_paths);
+
+ let (scan_job_tx, scan_job_rx) = channel::unbounded();
+ self.reload_entries_for_paths(
+ root_path,
+ root_canonical_path,
+ &relative_paths,
+ abs_paths,
+ Some(scan_job_tx.clone()),
+ )
+ .await;
+ drop(scan_job_tx);
+ self.scan_dirs(false, scan_job_rx).await;
+
+ let (scan_job_tx, scan_job_rx) = channel::unbounded();
+ self.update_ignore_statuses(scan_job_tx).await;
+ self.scan_dirs(false, scan_job_rx).await;
+
{
let mut state = self.state.lock();
- relative_paths.extend(unloaded_relative_paths);
state.reload_repositories(&relative_paths, self.fs.as_ref());
state.snapshot.completed_scan_id = state.snapshot.scan_id;
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
@@ -3610,23 +3648,28 @@ impl BackgroundScanner {
}
}
- let fs_entry = state.insert_entry(fs_entry, self.fs.as_ref());
-
- if let Some(scan_queue_tx) = &scan_queue_tx {
- let mut ancestor_inodes = state.snapshot.ancestor_inodes_for_path(&path);
- if metadata.is_dir && !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();
+ 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();
+ }
+ } else {
+ fs_entry.kind = EntryKind::UnloadedDir;
}
}
+
+ state.insert_entry(fs_entry, self.fs.as_ref());
}
Ok(None) => {
self.remove_repo_path(&path, &mut state.snapshot);
@@ -936,6 +936,119 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
);
}
+#[gpui::test]
+async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
+ let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+ let fs_fake = FakeFs::new(cx.background());
+ fs_fake
+ .insert_tree(
+ "/root",
+ json!({
+ "a": {},
+ }),
+ )
+ .await;
+
+ let tree_fake = Worktree::local(
+ client_fake,
+ "/root".as_ref(),
+ true,
+ fs_fake,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let entry = tree_fake
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.foreground().run_until_parked();
+ tree_fake.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+ assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ });
+
+ let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+ let fs_real = Arc::new(RealFs);
+ let temp_root = temp_tree(json!({
+ "a": {}
+ }));
+
+ let tree_real = Worktree::local(
+ client_real,
+ temp_root.path(),
+ true,
+ fs_real,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let entry = tree_real
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.foreground().run_until_parked();
+ tree_real.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+ assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ });
+
+ // Test smallest change
+ let entry = tree_real
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/e.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.foreground().run_until_parked();
+ tree_real.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/e.txt").unwrap().is_file());
+ });
+
+ // Test largest change
+ let entry = tree_real
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("d/e/f/g.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.foreground().run_until_parked();
+ tree_real.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("d/e/f/g.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("d/e/f").unwrap().is_dir());
+ assert!(tree.entry_for_path("d/e/").unwrap().is_dir());
+ assert!(tree.entry_for_path("d/").unwrap().is_dir());
+ });
+}
+
#[gpui::test(iterations = 100)]
async fn test_random_worktree_operations_during_initial_scan(
cx: &mut TestAppContext,
@@ -1654,37 +1767,37 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
}));
- let tree = Worktree::local(
- build_client(cx),
- root.path(),
- true,
- Arc::new(RealFs),
- Default::default(),
- &mut cx.to_async(),
- )
- .await
- .unwrap();
-
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
const A_TXT: &'static str = "a.txt";
const B_TXT: &'static str = "b.txt";
const E_TXT: &'static str = "c/d/e.txt";
const F_TXT: &'static str = "f.txt";
const DOTGITIGNORE: &'static str = ".gitignore";
const BUILD_FILE: &'static str = "target/build_file";
- let project_path: &Path = &Path::new("project");
+ let project_path = Path::new("project");
+ // Set up git repository before creating the worktree.
let work_dir = root.path().join("project");
let mut repo = git_init(work_dir.as_path());
repo.add_ignore_rule(IGNORE_RULE).unwrap();
- git_add(Path::new(A_TXT), &repo);
- git_add(Path::new(E_TXT), &repo);
- git_add(Path::new(DOTGITIGNORE), &repo);
+ git_add(A_TXT, &repo);
+ git_add(E_TXT, &repo);
+ git_add(DOTGITIGNORE, &repo);
git_commit("Initial commit", &repo);
+ let tree = Worktree::local(
+ build_client(cx),
+ root.path(),
+ true,
+ Arc::new(RealFs),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
deterministic.run_until_parked();
// Check that the right git state is observed on startup
@@ -1704,39 +1817,39 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
);
});
+ // Modify a file in the working copy.
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
-
tree.flush_fs_events(cx).await;
deterministic.run_until_parked();
+ // The worktree detects that the file's git status has changed.
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
-
assert_eq!(
snapshot.status_for_file(project_path.join(A_TXT)),
Some(GitFileStatus::Modified)
);
});
- git_add(Path::new(A_TXT), &repo);
- git_add(Path::new(B_TXT), &repo);
+ // Create a commit in the git repository.
+ git_add(A_TXT, &repo);
+ git_add(B_TXT, &repo);
git_commit("Committing modified and added", &repo);
tree.flush_fs_events(cx).await;
deterministic.run_until_parked();
- // Check that repo only changes are tracked
+ // The worktree detects that the files' git status have changed.
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
-
assert_eq!(
snapshot.status_for_file(project_path.join(F_TXT)),
Some(GitFileStatus::Added)
);
-
assert_eq!(snapshot.status_for_file(project_path.join(B_TXT)), None);
assert_eq!(snapshot.status_for_file(project_path.join(A_TXT)), None);
});
+ // Modify files in the working copy and perform git operations on other files.
git_reset(0, &repo);
git_remove_index(Path::new(B_TXT), &repo);
git_stash(&mut repo);
@@ -27,6 +27,7 @@ serde_derive.workspace = true
serde_json.workspace = true
anyhow.workspace = true
schemars.workspace = true
+pretty_assertions.workspace = true
unicase = "2.6"
[dev-dependencies]
@@ -64,7 +64,7 @@ pub struct ProjectPanel {
pending_serialization: Task<Option<()>>,
}
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug)]
struct Selection {
worktree_id: WorktreeId,
entry_id: ProjectEntryId,
@@ -547,7 +547,7 @@ impl ProjectPanel {
worktree_id,
entry_id: NEW_ENTRY_ID,
});
- let new_path = entry.path.join(&filename);
+ let new_path = entry.path.join(&filename.trim_start_matches("/"));
if path_already_exists(new_path.as_path()) {
return None;
}
@@ -588,6 +588,7 @@ impl ProjectPanel {
if selection.entry_id == edited_entry_id {
selection.worktree_id = worktree_id;
selection.entry_id = new_entry.id;
+ this.expand_to_selection(cx);
}
}
this.update_visible_entries(None, cx);
@@ -965,6 +966,24 @@ impl ProjectPanel {
Some((worktree, entry))
}
+ fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+ let (worktree, entry) = self.selected_entry(cx)?;
+ let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
+
+ for path in entry.path.ancestors() {
+ let Some(entry) = worktree.entry_for_path(path) else {
+ continue;
+ };
+ if entry.is_dir() {
+ if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
+ expanded_dir_ids.insert(idx, entry.id);
+ }
+ }
+ }
+
+ Some(())
+ }
+
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
@@ -1592,6 +1611,7 @@ impl ClipboardEntry {
mod tests {
use super::*;
use gpui::{TestAppContext, ViewHandle};
+ use pretty_assertions::assert_eq;
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
@@ -2002,6 +2022,133 @@ mod tests {
);
}
+ #[gpui::test(iterations = 30)]
+ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
+ },
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
+ },
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
+ },
+ "C": {
+ "5": {},
+ "6": { "V": "", "W": "" },
+ "7": { "X": "" },
+ "8": { "Y": {}, "Z": "" }
+ }
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "d": {
+ "9": ""
+ },
+ "e": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+ select_path(&panel, "root1", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1 <== selected",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ // Add a file with the root folder selected. The filename editor is placed
+ // before the first file in the root folder.
+ panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
+ cx.read_window(window_id, |cx| {
+ let panel = panel.read(cx);
+ assert!(panel.filename_editor.is_focused(cx));
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [EDITOR: ''] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ editor.set_text("/bdir1/dir2/the-new-filename", cx)
+ });
+ panel.confirm(&Confirm, cx).unwrap()
+ });
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..13, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " v bdir1",
+ " v dir2",
+ " the-new-filename <== selected",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+ }
+
#[gpui::test]
async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -21,6 +21,7 @@ util = { path = "../util"}
theme = { path = "../theme" }
workspace = { path = "../workspace" }
+futures.workspace = true
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
@@ -48,7 +48,7 @@ fn toggle(
let workspace = cx.weak_handle();
cx.add_view(|cx| {
RecentProjects::new(
- RecentProjectsDelegate::new(workspace, workspace_locations),
+ RecentProjectsDelegate::new(workspace, workspace_locations, true),
cx,
)
.with_max_size(800., 1200.)
@@ -64,25 +64,40 @@ fn toggle(
}))
}
-type RecentProjects = Picker<RecentProjectsDelegate>;
+pub fn build_recent_projects(
+ workspace: WeakViewHandle<Workspace>,
+ workspaces: Vec<WorkspaceLocation>,
+ cx: &mut ViewContext<RecentProjects>,
+) -> RecentProjects {
+ Picker::new(
+ RecentProjectsDelegate::new(workspace, workspaces, false),
+ cx,
+ )
+ .with_theme(|theme| theme.picker.clone())
+}
+
+pub type RecentProjects = Picker<RecentProjectsDelegate>;
-struct RecentProjectsDelegate {
+pub struct RecentProjectsDelegate {
workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize,
matches: Vec<StringMatch>,
+ render_paths: bool,
}
impl RecentProjectsDelegate {
fn new(
workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
+ render_paths: bool,
) -> Self {
Self {
workspace,
workspace_locations,
selected_match_index: 0,
matches: Default::default(),
+ render_paths,
}
}
}
@@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate {
highlighted_location
.paths
.into_iter()
+ .filter(|_| self.render_paths)
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
)
.flex(1., false)
@@ -38,5 +38,5 @@ tree-sitter-json = "*"
gpui = { path = "../gpui", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
indoc.workspace = true
-pretty_assertions = "1.3.0"
+pretty_assertions.workspace = true
unindent.workspace = true
@@ -1,7 +1,7 @@
use crate::{settings_store::parse_json_with_comments, SettingsAssets};
use anyhow::{anyhow, Context, Result};
use collections::BTreeMap;
-use gpui::{keymap_matcher::Binding, AppContext};
+use gpui::{keymap_matcher::Binding, AppContext, NoAction};
use schemars::{
gen::{SchemaGenerator, SchemaSettings},
schema::{InstanceType, Schema, SchemaObject, SingleOrVec, SubschemaValidation},
@@ -11,18 +11,18 @@ use serde::Deserialize;
use serde_json::Value;
use util::{asset_str, ResultExt};
-#[derive(Deserialize, Default, Clone, JsonSchema)]
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
#[serde(transparent)]
pub struct KeymapFile(Vec<KeymapBlock>);
-#[derive(Deserialize, Default, Clone, JsonSchema)]
+#[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
pub struct KeymapBlock {
#[serde(default)]
context: Option<String>,
bindings: BTreeMap<String, KeymapAction>,
}
-#[derive(Deserialize, Default, Clone)]
+#[derive(Debug, Deserialize, Default, Clone)]
#[serde(transparent)]
pub struct KeymapAction(Value);
@@ -61,21 +61,22 @@ impl KeymapFile {
// We want to deserialize the action data as a `RawValue` so that we can
// deserialize the action itself dynamically directly from the JSON
// string. But `RawValue` currently does not work inside of an untagged enum.
- if let Value::Array(items) = action {
- let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
- return Some(Err(anyhow!("Expected array of length 2")));
- };
- let serde_json::Value::String(name) = name else {
- return Some(Err(anyhow!("Expected first item in array to be a string.")))
- };
- cx.deserialize_action(
- &name,
- Some(data),
- )
- } else if let Value::String(name) = action {
- cx.deserialize_action(&name, None)
- } else {
- return Some(Err(anyhow!("Expected two-element array, got {:?}", action)));
+ match action {
+ Value::Array(items) => {
+ let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else {
+ return Some(Err(anyhow!("Expected array of length 2")));
+ };
+ let serde_json::Value::String(name) = name else {
+ return Some(Err(anyhow!("Expected first item in array to be a string.")))
+ };
+ cx.deserialize_action(
+ &name,
+ Some(data),
+ )
+ },
+ Value::String(name) => cx.deserialize_action(&name, None),
+ Value::Null => Ok(no_action()),
+ _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))),
}
.with_context(|| {
format!(
@@ -115,6 +116,10 @@ impl KeymapFile {
instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Array))),
..Default::default()
}),
+ Schema::Object(SchemaObject {
+ instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))),
+ ..Default::default()
+ }),
]),
..Default::default()
})),
@@ -129,6 +134,10 @@ impl KeymapFile {
}
}
+fn no_action() -> Box<dyn gpui::Action> {
+ Box::new(NoAction {})
+}
+
#[cfg(test)]
mod tests {
use crate::KeymapFile;
@@ -2489,7 +2489,12 @@ impl ToOffset for Point {
impl ToOffset for usize {
fn to_offset(&self, snapshot: &BufferSnapshot) -> usize {
- assert!(*self <= snapshot.len(), "offset {self} is out of range");
+ assert!(
+ *self <= snapshot.len(),
+ "offset {} is out of range, max allowed is {}",
+ self,
+ snapshot.len()
+ );
*self
}
}
@@ -65,7 +65,6 @@ pub struct Theme {
pub assistant: AssistantStyle,
pub feedback: FeedbackStyle,
pub welcome: WelcomeStyle,
- pub color_scheme: ColorScheme,
pub titlebar: Titlebar,
}
@@ -118,8 +117,9 @@ pub struct Titlebar {
#[serde(flatten)]
pub container: ContainerStyle,
pub height: f32,
- pub title: TextStyle,
- pub highlight_color: Color,
+ pub project_menu_button: Toggleable<Interactive<ContainedText>>,
+ pub project_name_divider: ContainedText,
+ pub git_menu_button: Toggleable<Interactive<ContainedText>>,
pub item_spacing: f32,
pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon,
@@ -585,6 +585,8 @@ pub struct Picker {
pub empty_input_editor: FieldEditor,
pub no_matches: ContainedLabel,
pub item: Toggleable<Interactive<ContainedLabel>>,
+ pub header: ContainedLabel,
+ pub footer: ContainedLabel,
}
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
@@ -720,6 +722,7 @@ pub struct Scrollbar {
pub width: f32,
pub min_height_factor: f32,
pub git: GitDiffColors,
+ pub selections: Color,
}
#[derive(Clone, Deserialize, Default, JsonSchema)]
@@ -36,7 +36,6 @@ workspace = { path = "../workspace" }
[dev-dependencies]
indoc.workspace = true
parking_lot.workspace = true
-lazy_static.workspace = true
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
@@ -11,8 +11,6 @@ use gpui::keymap_matcher::Keystroke;
use language::Point;
-#[cfg(feature = "neovim")]
-use lazy_static::lazy_static;
#[cfg(feature = "neovim")]
use nvim_rs::{
create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value,
@@ -32,9 +30,7 @@ use collections::VecDeque;
// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock
// to ensure we are only constructing one neovim connection at a time.
#[cfg(feature = "neovim")]
-lazy_static! {
- static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
-}
+static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(());
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum NeovimData {
@@ -97,9 +97,25 @@ lazy_static! {
}
pub trait Modal: View {
+ fn has_focus(&self) -> bool;
fn dismiss_on_event(event: &Self::Event) -> bool;
}
+trait ModalHandle {
+ fn as_any(&self) -> &AnyViewHandle;
+ fn has_focus(&self, cx: &WindowContext) -> bool;
+}
+
+impl<T: Modal> ModalHandle for ViewHandle<T> {
+ fn as_any(&self) -> &AnyViewHandle {
+ self
+ }
+
+ fn has_focus(&self, cx: &WindowContext) -> bool {
+ self.read(cx).has_focus()
+ }
+}
+
#[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId);
@@ -466,7 +482,7 @@ pub enum Event {
pub struct Workspace {
weak_self: WeakViewHandle<Self>,
remote_entity_subscription: Option<client::Subscription>,
- modal: Option<AnyViewHandle>,
+ modal: Option<ActiveModal>,
zoomed: Option<AnyWeakViewHandle>,
zoomed_position: Option<DockPosition>,
center: PaneGroup,
@@ -495,6 +511,11 @@ pub struct Workspace {
pane_history_timestamp: Arc<AtomicUsize>,
}
+struct ActiveModal {
+ view: Box<dyn ModalHandle>,
+ previously_focused_view_id: Option<usize>,
+}
+
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId {
pub creator: PeerId,
@@ -1482,8 +1503,10 @@ impl Workspace {
cx.notify();
// Whatever modal was visible is getting clobbered. If its the same type as V, then return
// it. Otherwise, create a new modal and set it as active.
- let already_open_modal = self.modal.take().and_then(|modal| modal.downcast::<V>());
- if let Some(already_open_modal) = already_open_modal {
+ if let Some(already_open_modal) = self
+ .dismiss_modal(cx)
+ .and_then(|modal| modal.downcast::<V>())
+ {
cx.focus_self();
Some(already_open_modal)
} else {
@@ -1494,8 +1517,12 @@ impl Workspace {
}
})
.detach();
+ let previously_focused_view_id = cx.focused_view_id();
cx.focus(&modal);
- self.modal = Some(modal.into_any());
+ self.modal = Some(ActiveModal {
+ view: Box::new(modal),
+ previously_focused_view_id,
+ });
None
}
}
@@ -1503,13 +1530,20 @@ impl Workspace {
pub fn modal<V: 'static + View>(&self) -> Option<ViewHandle<V>> {
self.modal
.as_ref()
- .and_then(|modal| modal.clone().downcast::<V>())
+ .and_then(|modal| modal.view.as_any().clone().downcast::<V>())
}
- pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) {
- if self.modal.take().is_some() {
- cx.focus(&self.active_pane);
+ pub fn dismiss_modal(&mut self, cx: &mut ViewContext<Self>) -> Option<AnyViewHandle> {
+ if let Some(modal) = self.modal.take() {
+ if let Some(previously_focused_view_id) = modal.previously_focused_view_id {
+ if modal.view.has_focus(cx) {
+ cx.window_context().focus(Some(previously_focused_view_id));
+ }
+ }
cx.notify();
+ Some(modal.view.as_any().clone())
+ } else {
+ None
}
}
@@ -3496,7 +3530,7 @@ impl View for Workspace {
)
}))
.with_children(self.modal.as_ref().map(|modal| {
- ChildView::new(modal, cx)
+ ChildView::new(modal.view.as_any(), cx)
.contained()
.with_style(theme.workspace.modal)
.aligned()
@@ -4775,6 +4809,7 @@ mod tests {
theme::init((), cx);
language::init(cx);
crate::init_settings(cx);
+ Project::init_settings(cx);
});
}
}
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.94.0"
+version = "0.95.0"
publish = false
[lib]
@@ -16,6 +16,7 @@ name = "Zed"
path = "src/main.rs"
[dependencies]
+audio = { path = "../audio" }
activity_indicator = { path = "../activity_indicator" }
auto_update = { path = "../auto_update" }
breadcrumbs = { path = "../breadcrumbs" }
@@ -7,6 +7,7 @@ use rust_embed::RustEmbed;
#[include = "fonts/**/*"]
#[include = "icons/**/*"]
#[include = "themes/**/*"]
+#[include = "sounds/**/*"]
#[include = "*.md"]
#[exclude = "*.DS_Store"]
pub struct Assets;
@@ -181,6 +181,8 @@ fn main() {
background_actions,
});
cx.set_global(Arc::downgrade(&app_state));
+
+ audio::init(Assets, cx);
auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx);
@@ -2074,6 +2074,167 @@ mod tests {
line!(),
);
+ #[track_caller]
+ fn assert_key_bindings_for<'a>(
+ window_id: usize,
+ cx: &TestAppContext,
+ actions: Vec<(&'static str, &'a dyn Action)>,
+ line: u32,
+ ) {
+ for (key, action) in actions {
+ // assert that...
+ assert!(
+ cx.available_actions(window_id, 0)
+ .into_iter()
+ .any(|(_, bound_action, b)| {
+ // action names match...
+ bound_action.name() == action.name()
+ && bound_action.namespace() == action.namespace()
+ // and key strokes contain the given key
+ && b.iter()
+ .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+ }),
+ "On {} Failed to find {} with key binding {}",
+ line,
+ action.name(),
+ key
+ );
+ }
+ }
+ }
+
+ #[gpui::test]
+ async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+ struct TestView;
+
+ impl Entity for TestView {
+ type Event = ();
+ }
+
+ impl View for TestView {
+ fn ui_name() -> &'static str {
+ "TestView"
+ }
+
+ fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+ Empty::new().into_any()
+ }
+ }
+
+ let executor = cx.background();
+ let fs = FakeFs::new(executor.clone());
+
+ actions!(test, [A, B]);
+ // From the Atom keymap
+ actions!(workspace, [ActivatePreviousPane]);
+ // From the JetBrains keymap
+ actions!(pane, [ActivatePrevItem]);
+
+ fs.save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "Atom"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ fs.save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": "test::A"
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init(Assets, cx);
+ welcome::init(cx);
+
+ cx.add_global_action(|_: &A, _cx| {});
+ cx.add_global_action(|_: &B, _cx| {});
+ cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+ cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+ let settings_rx = watch_config_file(
+ executor.clone(),
+ fs.clone(),
+ PathBuf::from("/settings.json"),
+ );
+ let keymap_rx =
+ watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+ handle_keymap_file_changes(keymap_rx, cx);
+ handle_settings_file_changes(settings_rx, cx);
+ });
+
+ cx.foreground().run_until_parked();
+
+ let (window_id, _view) = cx.add_window(|_| TestView);
+
+ // Test loading the keymap base at all
+ assert_key_bindings_for(
+ window_id,
+ cx,
+ vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+ line!(),
+ );
+
+ // Test disabling the key binding for the base keymap
+ fs.save(
+ "/keymap.json".as_ref(),
+ &r#"
+ [
+ {
+ "bindings": {
+ "backspace": null
+ }
+ }
+ ]
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.foreground().run_until_parked();
+
+ assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!());
+
+ // Test modifying the base, while retaining the users keymap
+ fs.save(
+ "/settings.json".as_ref(),
+ &r#"
+ {
+ "base_keymap": "JetBrains"
+ }
+ "#
+ .into(),
+ Default::default(),
+ )
+ .await
+ .unwrap();
+
+ cx.foreground().run_until_parked();
+
+ assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!());
+
+ #[track_caller]
fn assert_key_bindings_for<'a>(
window_id: usize,
cx: &TestAppContext,
@@ -2160,6 +2321,7 @@ mod tests {
state.initialize_workspace = initialize_workspace;
state.build_window_options = build_window_options;
theme::init((), cx);
+ audio::init((), cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
@@ -35,7 +35,7 @@ Match a property identifier and highlight it using the identifier `@property`. I
```
```ts
-function buildDefaultSyntax(colorScheme: ColorScheme): Partial<Syntax> {
+function buildDefaultSyntax(colorScheme: Theme): Partial<Syntax> {
// ...
}
```
@@ -27,7 +27,8 @@
"ts-node": "^10.9.1",
"typescript": "^5.1.5",
"utility-types": "^3.10.0",
- "vitest": "^0.32.0"
+ "vitest": "^0.32.0",
+ "zustand": "^4.3.8"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -2595,6 +2596,12 @@
"node": ">= 0.8"
}
},
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "peer": true
+ },
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@@ -2706,6 +2713,18 @@
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "peer": true,
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/loupe": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz",
@@ -3292,6 +3311,18 @@
}
]
},
+ "node_modules/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -4025,6 +4056,14 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+ "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/utility-types": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz",
@@ -4305,6 +4344,29 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.8.tgz",
+ "integrity": "sha512-4h28KCkHg5ii/wcFFJ5Fp+k1J3gJoasaIbppdgZFO4BPJnsNxL0mQXBSFgOgAdCdBj35aDTPvdAJReTMntFPGg==",
+ "dependencies": {
+ "use-sync-external-store": "1.2.0"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "immer": ">=9.0",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
}
}
}
@@ -16,21 +16,22 @@
"@tokens-studio/types": "^0.2.3",
"@types/chroma-js": "^2.4.0",
"@types/node": "^18.14.1",
+ "@typescript-eslint/eslint-plugin": "^5.60.1",
+ "@typescript-eslint/parser": "^5.60.1",
+ "@vitest/coverage-v8": "^0.32.0",
"ayu": "^8.0.1",
"chroma-js": "^2.4.2",
"deepmerge": "^4.3.0",
+ "eslint": "^8.43.0",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-import": "^2.27.5",
"json-schema-to-typescript": "^13.0.2",
"toml": "^3.0.0",
"ts-deepmerge": "^6.0.3",
"ts-node": "^10.9.1",
+ "typescript": "^5.1.5",
"utility-types": "^3.10.0",
"vitest": "^0.32.0",
- "@typescript-eslint/eslint-plugin": "^5.60.1",
- "@typescript-eslint/parser": "^5.60.1",
- "@vitest/coverage-v8": "^0.32.0",
- "eslint": "^8.43.0",
- "eslint-import-resolver-typescript": "^3.5.5",
- "eslint-plugin-import": "^2.27.5",
- "typescript": "^5.1.5"
+ "zustand": "^4.3.8"
}
}
@@ -2,8 +2,9 @@ import * as fs from "fs"
import { tmpdir } from "os"
import * as path from "path"
import app from "./style_tree/app"
-import { ColorScheme, create_color_scheme } from "./theme/color_scheme"
+import { Theme, create_theme } from "./theme/create_theme"
import { themes } from "./themes"
+import { useThemeStore } from "./theme"
const assets_directory = `${__dirname}/../../assets`
const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
@@ -20,15 +21,22 @@ function clear_themes(theme_directory: string) {
}
}
-function write_themes(themes: ColorScheme[], output_directory: string) {
+const all_themes: Theme[] = themes.map((theme) =>
+ create_theme(theme)
+)
+
+function write_themes(themes: Theme[], output_directory: string) {
clear_themes(output_directory)
- for (const color_scheme of themes) {
- const style_tree = app(color_scheme)
+ for (const theme of themes) {
+ const { setTheme } = useThemeStore.getState()
+ setTheme(theme)
+
+ const style_tree = app()
const style_tree_json = JSON.stringify(style_tree, null, 2)
- const temp_path = path.join(temp_directory, `${color_scheme.name}.json`)
+ const temp_path = path.join(temp_directory, `${theme.name}.json`)
const out_path = path.join(
output_directory,
- `${color_scheme.name}.json`
+ `${theme.name}.json`
)
fs.writeFileSync(temp_path, style_tree_json)
fs.renameSync(temp_path, out_path)
@@ -36,8 +44,4 @@ function write_themes(themes: ColorScheme[], output_directory: string) {
}
}
-const all_themes: ColorScheme[] = themes.map((theme) =>
- create_color_scheme(theme)
-)
-
write_themes(all_themes, `${assets_directory}/themes`)
@@ -1,9 +1,9 @@
import * as fs from "fs"
import * as path from "path"
-import { ColorScheme, create_color_scheme } from "./common"
+import { Theme, create_theme, useThemeStore } from "./common"
import { themes } from "./themes"
import { slugify } from "./utils/slugify"
-import { theme_tokens } from "./theme/tokens/color_scheme"
+import { theme_tokens } from "./theme/tokens/theme"
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
@@ -27,7 +27,7 @@ type TokenSet = {
selected_token_sets: { [key: string]: "enabled" }
}
-function build_token_set_order(theme: ColorScheme[]): {
+function build_token_set_order(theme: Theme[]): {
token_set_order: string[]
} {
const token_set_order: string[] = theme.map((scheme) =>
@@ -36,7 +36,7 @@ function build_token_set_order(theme: ColorScheme[]): {
return { token_set_order }
}
-function build_themes_index(theme: ColorScheme[]): TokenSet[] {
+function build_themes_index(theme: Theme[]): TokenSet[] {
const themes_index: TokenSet[] = theme.map((scheme, index) => {
const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
.toLowerCase()
@@ -55,12 +55,15 @@ function build_themes_index(theme: ColorScheme[]): TokenSet[] {
return themes_index
}
-function write_tokens(themes: ColorScheme[], tokens_directory: string) {
+function write_tokens(themes: Theme[], tokens_directory: string) {
clear_tokens(tokens_directory)
for (const theme of themes) {
+ const { setTheme } = useThemeStore.getState()
+ setTheme(theme)
+
const file_name = slugify(theme.name) + ".json"
- const tokens = theme_tokens(theme)
+ const tokens = theme_tokens()
const tokens_json = JSON.stringify(tokens, null, 2)
const out_path = path.join(tokens_directory, file_name)
fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
@@ -80,8 +83,8 @@ function write_tokens(themes: ColorScheme[], tokens_directory: string) {
console.log(`- ${METADATA_FILE} created`)
}
-const all_themes: ColorScheme[] = themes.map((theme) =>
- create_color_scheme(theme)
+const all_themes: Theme[] = themes.map((theme) =>
+ create_theme(theme)
)
write_tokens(all_themes, TOKENS_DIRECTORY)
@@ -1,6 +1,6 @@
import { interactive, toggleable } from "../element"
import { background, foreground } from "../style_tree/components"
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme, Theme } from "../theme"
export type Margin = {
top: number
@@ -11,21 +11,20 @@ export type Margin = {
interface IconButtonOptions {
layer?:
- | ColorScheme["lowest"]
- | ColorScheme["middle"]
- | ColorScheme["highest"]
- color?: keyof ColorScheme["lowest"]
+ | Theme["lowest"]
+ | Theme["middle"]
+ | Theme["highest"]
+ color?: keyof Theme["lowest"]
margin?: Partial<Margin>
}
type ToggleableIconButtonOptions = IconButtonOptions & {
- active_color?: keyof ColorScheme["lowest"]
+ active_color?: keyof Theme["lowest"]
}
-export function icon_button(
- theme: ColorScheme,
- { color, margin, layer }: IconButtonOptions
-) {
+export function icon_button({ color, margin, layer }: IconButtonOptions) {
+ const theme = useTheme()
+
if (!color) color = "base"
const m = {
@@ -68,15 +67,15 @@ export function icon_button(
}
export function toggleable_icon_button(
- theme: ColorScheme,
+ theme: Theme,
{ color, active_color, margin }: ToggleableIconButtonOptions
) {
if (!color) color = "base"
return toggleable({
state: {
- inactive: icon_button(theme, { color, margin }),
- active: icon_button(theme, {
+ inactive: icon_button({ color, margin }),
+ active: icon_button({
color: active_color ? active_color : color,
margin,
layer: theme.middle,
@@ -5,27 +5,30 @@ import {
foreground,
text,
} from "../style_tree/components"
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme, Theme } from "../theme"
import { Margin } from "./icon_button"
interface TextButtonOptions {
layer?:
- | ColorScheme["lowest"]
- | ColorScheme["middle"]
- | ColorScheme["highest"]
- color?: keyof ColorScheme["lowest"]
+ | Theme["lowest"]
+ | Theme["middle"]
+ | Theme["highest"]
+ color?: keyof Theme["lowest"]
margin?: Partial<Margin>
text_properties?: TextProperties
}
type ToggleableTextButtonOptions = TextButtonOptions & {
- active_color?: keyof ColorScheme["lowest"]
+ active_color?: keyof Theme["lowest"]
}
-export function text_button(
- theme: ColorScheme,
- { color, layer, margin, text_properties }: TextButtonOptions
-) {
+export function text_button({
+ color,
+ layer,
+ margin,
+ text_properties,
+}: TextButtonOptions) {
+ const theme = useTheme()
if (!color) color = "base"
const text_options: TextProperties = {
@@ -72,15 +75,15 @@ export function text_button(
}
export function toggleable_text_button(
- theme: ColorScheme,
+ theme: Theme,
{ color, active_color, margin }: ToggleableTextButtonOptions
) {
if (!color) color = "base"
return toggleable({
state: {
- inactive: text_button(theme, { color, margin }),
- active: text_button(theme, {
+ inactive: text_button({ color, margin }),
+ active: text_button({
color: active_color ? active_color : color,
margin,
layer: theme.middle,
@@ -17,59 +17,46 @@ import terminal from "./terminal"
import contact_list from "./contact_list"
import toolbar_dropdown_menu from "./toolbar_dropdown_menu"
import incoming_call_notification from "./incoming_call_notification"
-import { ColorScheme } from "../theme/color_scheme"
import welcome from "./welcome"
import copilot from "./copilot"
import assistant from "./assistant"
import { titlebar } from "./titlebar"
import editor from "./editor"
import feedback from "./feedback"
+import { useTheme } from "../common"
+
+export default function app(): any {
+ const theme = useTheme()
-export default function app(theme: ColorScheme): any {
return {
meta: {
name: theme.name,
is_light: theme.is_light,
},
- command_palette: command_palette(theme),
- contact_notification: contact_notification(theme),
- project_shared_notification: project_shared_notification(theme),
- incoming_call_notification: incoming_call_notification(theme),
- picker: picker(theme),
- workspace: workspace(theme),
- titlebar: titlebar(theme),
- copilot: copilot(theme),
- welcome: welcome(theme),
- context_menu: context_menu(theme),
- editor: editor(theme),
- project_diagnostics: project_diagnostics(theme),
- project_panel: project_panel(theme),
- contacts_popover: contacts_popover(theme),
- contact_finder: contact_finder(theme),
- contact_list: contact_list(theme),
- toolbar_dropdown_menu: toolbar_dropdown_menu(theme),
- search: search(theme),
- shared_screen: shared_screen(theme),
- update_notification: update_notification(theme),
- simple_message_notification: simple_message_notification(theme),
- tooltip: tooltip(theme),
- terminal: terminal(theme),
- assistant: assistant(theme),
- feedback: feedback(theme),
- color_scheme: {
- ...theme,
- players: Object.values(theme.players),
- ramps: {
- neutral: theme.ramps.neutral.colors(100, "hex"),
- red: theme.ramps.red.colors(100, "hex"),
- orange: theme.ramps.orange.colors(100, "hex"),
- yellow: theme.ramps.yellow.colors(100, "hex"),
- green: theme.ramps.green.colors(100, "hex"),
- cyan: theme.ramps.cyan.colors(100, "hex"),
- blue: theme.ramps.blue.colors(100, "hex"),
- violet: theme.ramps.violet.colors(100, "hex"),
- magenta: theme.ramps.magenta.colors(100, "hex"),
- },
- },
+ command_palette: command_palette(),
+ contact_notification: contact_notification(),
+ project_shared_notification: project_shared_notification(),
+ incoming_call_notification: incoming_call_notification(),
+ picker: picker(),
+ workspace: workspace(),
+ titlebar: titlebar(),
+ copilot: copilot(),
+ welcome: welcome(),
+ context_menu: context_menu(),
+ editor: editor(),
+ project_diagnostics: project_diagnostics(),
+ project_panel: project_panel(),
+ contacts_popover: contacts_popover(),
+ contact_finder: contact_finder(),
+ contact_list: contact_list(),
+ toolbar_dropdown_menu: toolbar_dropdown_menu(),
+ search: search(),
+ shared_screen: shared_screen(),
+ update_notification: update_notification(),
+ simple_message_notification: simple_message_notification(),
+ tooltip: tooltip(),
+ terminal: terminal(),
+ assistant: assistant(),
+ feedback: feedback()
}
}
@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
import { text, border, background, foreground } from "./components"
import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function assistant(): any {
+ const theme = useTheme()
-export default function assistant(theme: ColorScheme): any {
return {
container: {
background: background(theme.highest),
@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color"
import { text, background } from "./components"
import { toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function command_palette(): any {
+ const theme = useTheme()
-export default function command_palette(theme: ColorScheme): any {
const key = toggleable({
base: {
text: text(theme.highest, "mono", "variant", "default", {
@@ -1,5 +1,5 @@
import { font_families, font_sizes, FontWeight } from "../common"
-import { Layer, Styles, StyleSets, Style } from "../theme/color_scheme"
+import { Layer, Styles, StyleSets, Style } from "../theme/create_theme"
function is_style_set(key: any): key is StyleSets {
return [
@@ -1,8 +1,10 @@
import picker from "./picker"
-import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, text } from "./components"
+import { useTheme } from "../theme"
+
+export default function contact_finder(): any {
+ const theme = useTheme()
-export default function contact_finder(theme: ColorScheme): any {
const side_margin = 6
const contact_button = {
background: background(theme.middle, "variant"),
@@ -12,7 +14,7 @@ export default function contact_finder(theme: ColorScheme): any {
corner_radius: 8,
}
- const picker_style = picker(theme)
+ const picker_style = picker()
const picker_input = {
background: background(theme.middle, "on"),
corner_radius: 6,
@@ -44,6 +46,8 @@ export default function contact_finder(theme: ColorScheme): any {
no_matches: picker_style.no_matches,
input_editor: picker_input,
empty_input_editor: picker_input,
+ header: picker_style.header,
+ footer: picker_style.footer,
},
row_height: 28,
contact_avatar: {
@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
import {
background,
border,
@@ -7,7 +6,10 @@ import {
text,
} from "./components"
import { interactive, toggleable } from "../element"
-export default function contacts_panel(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function contacts_panel(): any {
+ const theme = useTheme()
+
const name_margin = 8
const side_padding = 12
@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
import { background, foreground, text } from "./components"
import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function contact_notification(): any {
+ const theme = useTheme()
-export default function contact_notification(theme: ColorScheme): any {
const avatar_size = 12
const header_padding = 8
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
import { background, border } from "./components"
-export default function contacts_popover(theme: ColorScheme): any {
+export default function contacts_popover(): any {
+ const theme = useTheme()
+
return {
background: background(theme.middle),
corner_radius: 6,
@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
import { background, border, border_color, text } from "./components"
import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function context_menu(): any {
+ const theme = useTheme()
-export default function context_menu(theme: ColorScheme): any {
return {
background: background(theme.middle),
corner_radius: 10,
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, svg, text } from "./components"
import { interactive } from "../element"
-export default function copilot(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function copilot(): any {
+ const theme = useTheme()
+
const content_width = 264
const cta_button =
@@ -1,5 +1,5 @@
import { with_opacity } from "../theme/color"
-import { ColorScheme, Layer, StyleSets } from "../theme/color_scheme"
+import { Layer, StyleSets } from "../theme/create_theme"
import {
background,
border,
@@ -11,8 +11,11 @@ import hover_popover from "./hover_popover"
import { build_syntax } from "../theme/syntax"
import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function editor(): any {
+ const theme = useTheme()
-export default function editor(theme: ColorScheme): any {
const { is_light } = theme
const layer = theme.highest
@@ -45,7 +48,7 @@ export default function editor(theme: ColorScheme): any {
}
}
- const syntax = build_syntax(theme)
+ const syntax = build_syntax()
return {
text_color: syntax.primary.color,
@@ -248,7 +251,7 @@ export default function editor(theme: ColorScheme): any {
invalid_hint_diagnostic: diagnostic(theme.middle, "base"),
invalid_information_diagnostic: diagnostic(theme.middle, "base"),
invalid_warning_diagnostic: diagnostic(theme.middle, "base"),
- hover_popover: hover_popover(theme),
+ hover_popover: hover_popover(),
link_definition: {
color: syntax.link_uri.color,
underline: syntax.link_uri.underline,
@@ -301,6 +304,7 @@ export default function editor(theme: ColorScheme): any {
? with_opacity(theme.ramps.green(0.5).hex(), 0.8)
: with_opacity(theme.ramps.green(0.4).hex(), 0.8),
},
+ selections: foreground(layer, "accent")
},
composition_mark: {
underline: {
@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
import { background, border, text } from "./components"
import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function feedback(): any {
+ const theme = useTheme()
-export default function feedback(theme: ColorScheme): any {
return {
submit_button: interactive({
base: {
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
import { background, border, foreground, text } from "./components"
-export default function hover_popover(theme: ColorScheme): any {
+export default function hover_popover(): any {
+ const theme = useTheme()
+
const base_container = {
background: background(theme.middle),
corner_radius: 8,
@@ -1,9 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
import { background, border, text } from "./components"
-export default function incoming_call_notification(
- theme: ColorScheme
-): unknown {
+export default function incoming_call_notification(): unknown {
+ const theme = useTheme()
+
const avatar_size = 48
return {
window_height: 74,
@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color"
import { background, border, text } from "./components"
import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function picker(): any {
+ const theme = useTheme()
-export default function picker(theme: ColorScheme): any {
const container = {
background: background(theme.lowest),
border: border(theme.lowest),
@@ -108,5 +110,23 @@ export default function picker(theme: ColorScheme): any {
top: 8,
},
},
+ header: {
+ text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+
+ margin: {
+ top: 1,
+ left: 8,
+ right: 8,
+ },
+ },
+ footer: {
+ text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+ margin: {
+ top: 1,
+ left: 8,
+ right: 8,
+ },
+
+ }
}
}
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
import { background, text } from "./components"
-export default function project_diagnostics(theme: ColorScheme): any {
+export default function project_diagnostics(): any {
+ const theme = useTheme()
+
return {
background: background(theme.highest),
tab_icon_spacing: 4,
@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color"
import {
Border,
@@ -10,7 +9,10 @@ import {
} from "./components"
import { interactive, toggleable } from "../element"
import merge from "ts-deepmerge"
-export default function project_panel(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function project_panel(): any {
+ const theme = useTheme()
+
const { is_light } = theme
type EntryStateProps = {
@@ -65,13 +67,12 @@ export default function project_panel(theme: ColorScheme): any {
const unselected_hovered_style = merge(
base_properties,
{ background: background(theme.middle, "hovered") },
- unselected?.hovered ?? {},
+ unselected?.hovered ?? {}
)
const unselected_clicked_style = merge(
base_properties,
- { background: background(theme.middle, "pressed"), }
- ,
- unselected?.clicked ?? {},
+ { background: background(theme.middle, "pressed") },
+ unselected?.clicked ?? {}
)
const selected_default_style = merge(
base_properties,
@@ -79,18 +80,15 @@ export default function project_panel(theme: ColorScheme): any {
background: background(theme.lowest),
text: text(theme.lowest, "sans", { size: "sm" }),
},
- selected_style?.default ?? {},
-
+ selected_style?.default ?? {}
)
const selected_hovered_style = merge(
base_properties,
{
background: background(theme.lowest, "hovered"),
text: text(theme.lowest, "sans", { size: "sm" }),
-
},
- selected_style?.hovered ?? {},
-
+ selected_style?.hovered ?? {}
)
const selected_clicked_style = merge(
base_properties,
@@ -98,8 +96,7 @@ export default function project_panel(theme: ColorScheme): any {
background: background(theme.lowest, "pressed"),
text: text(theme.lowest, "sans", { size: "sm" }),
},
- selected_style?.clicked ?? {},
-
+ selected_style?.clicked ?? {}
)
return toggleable({
@@ -1,9 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
import { background, border, text } from "./components"
-export default function project_shared_notification(
- theme: ColorScheme
-): unknown {
+export default function project_shared_notification(): unknown {
+ const theme = useTheme()
+
const avatar_size = 48
return {
window_height: 74,
@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color"
import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
+
+export default function search(): any {
+ const theme = useTheme()
-export default function search(theme: ColorScheme): any {
// Search input
const editor = {
background: background(theme.highest),
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
import { background } from "./components"
-export default function sharedScreen(theme: ColorScheme) {
+export default function sharedScreen() {
+ const theme = useTheme()
+
return {
background: background(theme.highest),
}
@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, text } from "./components"
import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function simple_message_notification(): any {
+ const theme = useTheme()
-export default function simple_message_notification(theme: ColorScheme): any {
const header_padding = 8
return {
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
import { background, border, foreground, text } from "./components"
import { interactive, toggleable } from "../element"
-export default function status_bar(theme: ColorScheme): any {
+import { useTheme } from "../common"
+export default function status_bar(): any {
+ const theme = useTheme()
+
const layer = theme.lowest
const status_container = {
@@ -1,9 +1,11 @@
-import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color"
import { text, border, background, foreground } from "./components"
import { interactive, toggleable } from "../element"
+import { useTheme } from "../common"
+
+export default function tab_bar(): any {
+ const theme = useTheme()
-export default function tab_bar(theme: ColorScheme): any {
const height = 32
const active_layer = theme.highest
@@ -1,6 +1,8 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
+
+export default function terminal() {
+ const theme = useTheme()
-export default function terminal(theme: ColorScheme) {
/**
* Colors are controlled per-cell in the terminal grid.
* Cells can be set to any of these more 'theme-capable' colors
@@ -1,7 +1,7 @@
-import { ColorScheme } from "../common"
import { icon_button, toggleable_icon_button } from "../component/icon_button"
import { toggleable_text_button } from "../component/text_button"
import { interactive, toggleable } from "../element"
+import { useTheme } from "../theme"
import { with_opacity } from "../theme/color"
import { background, border, foreground, text } from "./components"
@@ -22,7 +22,9 @@ function build_spacing(
}
}
-function call_controls(theme: ColorScheme) {
+function call_controls() {
+ const theme = useTheme()
+
const button_height = 18
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
@@ -69,7 +71,9 @@ function call_controls(theme: ColorScheme) {
* When logged in shows the user's avatar and a chevron,
* When logged out only shows a chevron.
*/
-function user_menu(theme: ColorScheme) {
+function user_menu() {
+ const theme = useTheme()
+
const button_height = 18
const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING)
@@ -155,7 +159,9 @@ function user_menu(theme: ColorScheme) {
}
}
-export function titlebar(theme: ColorScheme): any {
+export function titlebar(): any {
+ const theme = useTheme()
+
const avatar_width = 15
const avatar_outer_width = avatar_width + 4
const follower_avatar_width = 14
@@ -173,8 +179,14 @@ export function titlebar(theme: ColorScheme): any {
},
// Project
- title: text(theme.lowest, "sans", "variant"),
- highlight_color: text(theme.lowest, "sans", "active").color,
+ project_name_divider: text(theme.lowest, "sans", "variant"),
+
+ project_menu_button: toggleable_text_button(theme, {
+ color: 'base',
+ }),
+ git_menu_button: toggleable_text_button(theme, {
+ color: 'variant',
+ }),
// Collaborators
leader_avatar: {
@@ -237,14 +249,14 @@ export function titlebar(theme: ColorScheme): any {
corner_radius: 6,
},
- leave_call_button: icon_button(theme, {
+ leave_call_button: icon_button({
margin: {
left: ITEM_SPACING / 2,
right: ITEM_SPACING,
},
}),
- ...call_controls(theme),
+ ...call_controls(),
toggle_contacts_button: toggleable_icon_button(theme, {
margin: {
@@ -261,6 +273,6 @@ export function titlebar(theme: ColorScheme): any {
background: foreground(theme.lowest, "accent"),
},
share_button: toggleable_text_button(theme, {}),
- user_menu: user_menu(theme),
+ user_menu: user_menu(),
}
}
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
import { background, border, text } from "./components"
import { interactive, toggleable } from "../element"
-export default function dropdown_menu(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+export default function dropdown_menu(): any {
+ const theme = useTheme()
+
return {
row_height: 30,
background: background(theme.middle),
@@ -1,7 +1,9 @@
-import { ColorScheme } from "../theme/color_scheme"
+import { useTheme } from "../theme"
import { background, border, text } from "./components"
-export default function tooltip(theme: ColorScheme): any {
+export default function tooltip(): any {
+ const theme = useTheme()
+
return {
background: background(theme.middle),
border: border(theme.middle),
@@ -1,8 +1,10 @@
-import { ColorScheme } from "../theme/color_scheme"
import { foreground, text } from "./components"
import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function update_notification(): any {
+ const theme = useTheme()
-export default function update_notification(theme: ColorScheme): any {
const header_padding = 8
return {
@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color"
import {
border,
@@ -9,8 +8,11 @@ import {
svg,
} from "./components"
import { interactive } from "../element"
+import { useTheme } from "../theme"
+
+export default function welcome(): any {
+ const theme = useTheme()
-export default function welcome(theme: ColorScheme): any {
const checkbox_base = {
corner_radius: 4,
padding: {
@@ -1,4 +1,3 @@
-import { ColorScheme } from "../theme/color_scheme"
import { with_opacity } from "../theme/color"
import {
background,
@@ -11,9 +10,12 @@ import {
import statusBar from "./status_bar"
import tabBar from "./tab_bar"
import { interactive } from "../element"
-
import { titlebar } from "./titlebar"
-export default function workspace(theme: ColorScheme): any {
+import { useTheme } from "../theme"
+
+export default function workspace(): any {
+ const theme = useTheme()
+
const { is_light } = theme
return {
@@ -85,7 +87,7 @@ export default function workspace(theme: ColorScheme): any {
},
leader_border_opacity: 0.7,
leader_border_width: 2.0,
- tab_bar: tabBar(theme),
+ tab_bar: tabBar(),
modal: {
margin: {
bottom: 52,
@@ -123,8 +125,8 @@ export default function workspace(theme: ColorScheme): any {
color: border_color(theme.lowest),
width: 1,
},
- status_bar: statusBar(theme),
- titlebar: titlebar(theme),
+ status_bar: statusBar(),
+ titlebar: titlebar(),
toolbar: {
height: 34,
background: background(theme.highest),
@@ -8,7 +8,7 @@ import {
} from "./theme_config"
import { get_ramps } from "./ramps"
-export interface ColorScheme {
+export interface Theme {
name: string
is_light: boolean
@@ -105,7 +105,7 @@ export interface Style {
foreground: string
}
-export function create_color_scheme(theme: ThemeConfig): ColorScheme {
+export function create_theme(theme: ThemeConfig): Theme {
const {
name,
appearance,
@@ -1,4 +1,25 @@
-export * from "./color_scheme"
+import { create } from "zustand"
+import { Theme } from "./create_theme"
+
+type ThemeState = {
+ theme: Theme | undefined
+ setTheme: (theme: Theme) => void
+}
+
+export const useThemeStore = create<ThemeState>((set) => ({
+ theme: undefined,
+ setTheme: (theme) => set(() => ({ theme })),
+}))
+
+export const useTheme = (): Theme => {
+ const { theme } = useThemeStore.getState()
+
+ if (!theme) throw new Error("Tried to use theme before it was loaded")
+
+ return theme
+}
+
+export * from "./create_theme"
export * from "./ramps"
export * from "./syntax"
export * from "./theme_config"
@@ -1,5 +1,5 @@
import chroma, { Color, Scale } from "chroma-js"
-import { RampSet } from "./color_scheme"
+import { RampSet } from "./create_theme"
import {
ThemeConfigInputColors,
ThemeConfigInputColorsKeys,
@@ -1,6 +1,5 @@
import deepmerge from "deepmerge"
-import { FontWeight, font_weights } from "../common"
-import { ColorScheme } from "./color_scheme"
+import { FontWeight, font_weights, useTheme } from "../common"
import chroma from "chroma-js"
export interface SyntaxHighlightStyle {
@@ -123,7 +122,9 @@ const default_syntax_highlight_style: Omit<SyntaxHighlightStyle, "color"> = {
italic: false,
}
-function build_default_syntax(color_scheme: ColorScheme): Syntax {
+function build_default_syntax(): Syntax {
+ const theme = useTheme()
+
// Make a temporary object that is allowed to be missing
// the "color" property for each style
const syntax: {
@@ -141,8 +142,8 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
// predictive color distinct from any other color in the theme
const predictive = chroma
.mix(
- color_scheme.ramps.neutral(0.4).hex(),
- color_scheme.ramps.blue(0.4).hex(),
+ theme.ramps.neutral(0.4).hex(),
+ theme.ramps.blue(0.4).hex(),
0.45,
"lch"
)
@@ -151,32 +152,32 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
// hint color distinct from any other color in the theme
const hint = chroma
.mix(
- color_scheme.ramps.neutral(0.6).hex(),
- color_scheme.ramps.blue(0.4).hex(),
+ theme.ramps.neutral(0.6).hex(),
+ theme.ramps.blue(0.4).hex(),
0.45,
"lch"
)
.hex()
const color = {
- primary: color_scheme.ramps.neutral(1).hex(),
- comment: color_scheme.ramps.neutral(0.71).hex(),
- punctuation: color_scheme.ramps.neutral(0.86).hex(),
+ primary: theme.ramps.neutral(1).hex(),
+ comment: theme.ramps.neutral(0.71).hex(),
+ punctuation: theme.ramps.neutral(0.86).hex(),
predictive: predictive,
hint: hint,
- emphasis: color_scheme.ramps.blue(0.5).hex(),
- string: color_scheme.ramps.orange(0.5).hex(),
- function: color_scheme.ramps.yellow(0.5).hex(),
- type: color_scheme.ramps.cyan(0.5).hex(),
- constructor: color_scheme.ramps.blue(0.5).hex(),
- variant: color_scheme.ramps.blue(0.5).hex(),
- property: color_scheme.ramps.blue(0.5).hex(),
- enum: color_scheme.ramps.orange(0.5).hex(),
- operator: color_scheme.ramps.orange(0.5).hex(),
- number: color_scheme.ramps.green(0.5).hex(),
- boolean: color_scheme.ramps.green(0.5).hex(),
- constant: color_scheme.ramps.green(0.5).hex(),
- keyword: color_scheme.ramps.blue(0.5).hex(),
+ emphasis: theme.ramps.blue(0.5).hex(),
+ string: theme.ramps.orange(0.5).hex(),
+ function: theme.ramps.yellow(0.5).hex(),
+ type: theme.ramps.cyan(0.5).hex(),
+ constructor: theme.ramps.blue(0.5).hex(),
+ variant: theme.ramps.blue(0.5).hex(),
+ property: theme.ramps.blue(0.5).hex(),
+ enum: theme.ramps.orange(0.5).hex(),
+ operator: theme.ramps.orange(0.5).hex(),
+ number: theme.ramps.green(0.5).hex(),
+ boolean: theme.ramps.green(0.5).hex(),
+ constant: theme.ramps.green(0.5).hex(),
+ keyword: theme.ramps.blue(0.5).hex(),
}
// Then assign colors and use Syntax to enforce each style getting it's own color
@@ -211,11 +212,11 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
weight: font_weights.bold,
},
link_uri: {
- color: color_scheme.ramps.green(0.5).hex(),
+ color: theme.ramps.green(0.5).hex(),
underline: true,
},
link_text: {
- color: color_scheme.ramps.orange(0.5).hex(),
+ color: theme.ramps.orange(0.5).hex(),
italic: true,
},
"text.literal": {
@@ -231,7 +232,7 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
color: color.punctuation,
},
"punctuation.special": {
- color: color_scheme.ramps.neutral(0.86).hex(),
+ color: theme.ramps.neutral(0.86).hex(),
},
"punctuation.list_marker": {
color: color.punctuation,
@@ -252,10 +253,10 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
color: color.string,
},
constructor: {
- color: color_scheme.ramps.blue(0.5).hex(),
+ color: theme.ramps.blue(0.5).hex(),
},
variant: {
- color: color_scheme.ramps.blue(0.5).hex(),
+ color: theme.ramps.blue(0.5).hex(),
},
type: {
color: color.type,
@@ -264,16 +265,16 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
color: color.primary,
},
label: {
- color: color_scheme.ramps.blue(0.5).hex(),
+ color: theme.ramps.blue(0.5).hex(),
},
tag: {
- color: color_scheme.ramps.blue(0.5).hex(),
+ color: theme.ramps.blue(0.5).hex(),
},
attribute: {
- color: color_scheme.ramps.blue(0.5).hex(),
+ color: theme.ramps.blue(0.5).hex(),
},
property: {
- color: color_scheme.ramps.blue(0.5).hex(),
+ color: theme.ramps.blue(0.5).hex(),
},
constant: {
color: color.constant,
@@ -307,17 +308,18 @@ function build_default_syntax(color_scheme: ColorScheme): Syntax {
return default_syntax
}
-function merge_syntax(
- default_syntax: Syntax,
- color_scheme: ColorScheme
-): Syntax {
- if (!color_scheme.syntax) {
+export function build_syntax(): Syntax {
+ const theme = useTheme()
+
+ const default_syntax: Syntax = build_default_syntax()
+
+ if (!theme.syntax) {
return default_syntax
}
- return deepmerge<Syntax, Partial<ThemeSyntax>>(
+ const syntax = deepmerge<Syntax, Partial<ThemeSyntax>>(
default_syntax,
- color_scheme.syntax,
+ theme.syntax,
{
arrayMerge: (destinationArray, sourceArray) => [
...destinationArray,
@@ -325,12 +327,6 @@ function merge_syntax(
],
}
)
-}
-
-export function build_syntax(color_scheme: ColorScheme): Syntax {
- const default_syntax: Syntax = build_default_syntax(color_scheme)
-
- const syntax = merge_syntax(default_syntax, color_scheme)
return syntax
}
@@ -66,35 +66,10 @@ type ThemeConfigProperties = ThemeMeta & {
override: ThemeConfigOverrides
}
-// This should be the format a theme is defined as
export type ThemeConfig = {
[K in keyof ThemeConfigProperties]: ThemeConfigProperties[K]
}
-interface ThemeColors {
- neutral: string[]
- red: string[]
- orange: string[]
- yellow: string[]
- green: string[]
- cyan: string[]
- blue: string[]
- violet: string[]
- magenta: string[]
-}
-
-type ThemeSyntax = Required<Syntax>
-
-export type ThemeProperties = ThemeMeta & {
- color: ThemeColors
- syntax: ThemeSyntax
-}
-
-// This should be a theme after all its properties have been resolved
-export type Theme = {
- [K in keyof ThemeProperties]: ThemeProperties[K]
-}
-
export enum ThemeAppearance {
Light = "light",
Dark = "dark",
@@ -104,45 +79,3 @@ export enum ThemeLicenseType {
MIT = "MIT",
Apache2 = "Apache License 2.0",
}
-
-export type ThemeFamilyItem =
- | ThemeConfig
- | { light: ThemeConfig; dark: ThemeConfig }
-
-type ThemeFamilyProperties = Partial<Omit<ThemeMeta, "name" | "appearance">> & {
- name: string
- default: ThemeFamilyItem
- variants: {
- [key: string]: ThemeFamilyItem
- }
-}
-
-// Idea: A theme family is a collection of themes that share the same name
-// For example, a theme family could be `One Dark` and have a `light` and `dark` variant
-// The Ayu family could have `light`, `mirage`, and `dark` variants
-
-type ThemeFamily = {
- [K in keyof ThemeFamilyProperties]: ThemeFamilyProperties[K]
-}
-
-/** The collection of all themes
- *
- * Example:
- * ```ts
- * {
- * one_dark,
- * one_light,
- * ayu: {
- * name: 'Ayu',
- * default: 'ayu_mirage',
- * variants: {
- * light: 'ayu_light',
- * mirage: 'ayu_mirage',
- * dark: 'ayu_dark',
- * },
- * },
- * ...
- * }
- * ```
- */
-export type ThemeIndex = Record<string, ThemeFamily | ThemeConfig>
@@ -1,5 +1,5 @@
import { SingleColorToken } from "@tokens-studio/types"
-import { Layer, Style, StyleSet } from "../color_scheme"
+import { Layer, Style, StyleSet } from "../create_theme"
import { color_token } from "./token"
interface StyleToken {
@@ -1,12 +1,14 @@
import { SingleColorToken } from "@tokens-studio/types"
import { color_token } from "./token"
-import { ColorScheme, Players } from "../color_scheme"
+import { Players } from "../create_theme"
+import { useTheme } from "../../../src/common"
export type PlayerToken = Record<"selection" | "cursor", SingleColorToken>
export type PlayersToken = Record<keyof Players, PlayerToken>
-function build_player_token(theme: ColorScheme, index: number): PlayerToken {
+function build_player_token(index: number): PlayerToken {
+ const theme = useTheme()
const player_number = index.toString() as keyof Players
return {
@@ -21,13 +23,15 @@ function build_player_token(theme: ColorScheme, index: number): PlayerToken {
}
}
-export const players_token = (theme: ColorScheme): PlayersToken => ({
- "0": build_player_token(theme, 0),
- "1": build_player_token(theme, 1),
- "2": build_player_token(theme, 2),
- "3": build_player_token(theme, 3),
- "4": build_player_token(theme, 4),
- "5": build_player_token(theme, 5),
- "6": build_player_token(theme, 6),
- "7": build_player_token(theme, 7),
-})
+export const players_token = (): PlayersToken => {
+ return {
+ "0": build_player_token(0),
+ "1": build_player_token(1),
+ "2": build_player_token(2),
+ "3": build_player_token(3),
+ "4": build_player_token(4),
+ "5": build_player_token(5),
+ "6": build_player_token(6),
+ "7": build_player_token(7),
+ }
+}
@@ -5,18 +5,18 @@ import {
TokenTypes,
} from "@tokens-studio/types"
import {
- ColorScheme,
Shadow,
SyntaxHighlightStyle,
ThemeSyntax,
-} from "../color_scheme"
+} from "../create_theme"
import { LayerToken, layer_token } from "./layer"
import { PlayersToken, players_token } from "./players"
import { color_token } from "./token"
import { Syntax } from "../syntax"
import editor from "../../style_tree/editor"
+import { useTheme } from "../../../src/common"
-interface ColorSchemeTokens {
+interface ThemeTokens {
name: SingleOtherToken
appearance: SingleOtherToken
lowest: LayerToken
@@ -39,12 +39,14 @@ const create_shadow_token = (
}
}
-const popover_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => {
+const popover_shadow_token = (): SingleBoxShadowToken => {
+ const theme = useTheme()
const shadow = theme.popover_shadow
return create_shadow_token(shadow, "popover_shadow")
}
-const modal_shadow_token = (theme: ColorScheme): SingleBoxShadowToken => {
+const modal_shadow_token = (): SingleBoxShadowToken => {
+ const theme = useTheme()
const shadow = theme.modal_shadow
return create_shadow_token(shadow, "modal_shadow")
}
@@ -68,13 +70,15 @@ function syntax_highlight_style_color_tokens(
}, {} as ThemeSyntaxColorTokens)
}
-const syntax_tokens = (theme: ColorScheme): ColorSchemeTokens["syntax"] => {
- const syntax = editor(theme).syntax
+const syntax_tokens = (): ThemeTokens["syntax"] => {
+ const syntax = editor().syntax
return syntax_highlight_style_color_tokens(syntax)
}
-export function theme_tokens(theme: ColorScheme): ColorSchemeTokens {
+export function theme_tokens(): ThemeTokens {
+ const theme = useTheme()
+
return {
name: {
name: "themeName",
@@ -89,9 +93,9 @@ export function theme_tokens(theme: ColorScheme): ColorSchemeTokens {
lowest: layer_token(theme.lowest, "lowest"),
middle: layer_token(theme.middle, "middle"),
highest: layer_token(theme.highest, "highest"),
- popover_shadow: popover_shadow_token(theme),
- modal_shadow: modal_shadow_token(theme),
- players: players_token(theme),
- syntax: syntax_tokens(theme),
+ popover_shadow: popover_shadow_token(),
+ modal_shadow: modal_shadow_token(),
+ players: players_token(),
+ syntax: syntax_tokens(),
}
}
@@ -22,17 +22,9 @@
"strictPropertyInitialization": false,
"skipLibCheck": true,
"useUnknownInCatchVariables": false,
- "baseUrl": ".",
- "paths": {
- "@/*": ["./*"],
- "@element/*": ["./src/element/*"],
- "@component/*": ["./src/component/*"],
- "@styleTree/*": ["./src/styleTree/*"],
- "@theme/*": ["./src/theme/*"],
- "@types/*": ["./src/util/*"],
- "@themes/*": ["./src/themes/*"],
- "@util/*": ["./src/util/*"]
- }
+ "baseUrl": "."
},
- "exclude": ["node_modules"]
+ "exclude": [
+ "node_modules"
+ ]
}