diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000..35049cbcb1 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000000..b05d68911f --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,6 @@ +[test-groups] +sequential-db-tests = { max-threads = 1 } + +[[profile.default.overrides]] +filter = 'package(db)' +test-group = 'sequential-db-tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1c16b2d4d..a906c8b82d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,7 @@ jobs: rustup set profile minimal rustup update stable rustup target add wasm32-wasi + cargo install cargo-nextest - name: Install Node uses: actions/setup-node@v2 @@ -70,7 +71,7 @@ jobs: run: cargo check --workspace - name: Run tests - run: cargo test --workspace --no-fail-fast + run: cargo nextest run --workspace --no-fail-fast - name: Build collab run: cargo build -p collab diff --git a/.gitignore b/.gitignore index 5a4d2ff25e..dbffa0f829 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ /plugins/bin /script/node_modules /styles/node_modules +/styles/src/types/zed.ts +/crates/theme/schemas/theme.json /crates/collab/static/styles.css /vendor/bin /assets/themes/*.json @@ -18,4 +20,5 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +.swiftpm **/*.db diff --git a/Cargo.lock b/Cargo.lock index 24fd67f90e..ac089cee18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,11 +109,14 @@ dependencies = [ "isahc", "language", "menu", + "project", + "regex", "schemars", "search", "serde", "serde_json", "settings", + "smol", "theme", "tiktoken-rs", "util", @@ -174,6 +177,28 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" +[[package]] +name = "alsa" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" +dependencies = [ + "alsa-sys", + "bitflags", + "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" @@ -189,6 +214,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal 0.4.7", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.71" @@ -538,6 +612,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" @@ -593,7 +680,7 @@ dependencies = [ "http", "http-body", "hyper", - "itoa", + "itoa 1.0.6", "matchit", "memchr", "mime", @@ -704,6 +791,26 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags", + "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" @@ -805,7 +912,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", ] @@ -934,6 +1041,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-broadcast", + "audio", "client", "collections", "fs", @@ -1030,6 +1138,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" @@ -1101,15 +1215,39 @@ checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ "atty", "bitflags", - "clap_derive", - "clap_lex", - "indexmap", + "clap_derive 3.2.25", + "clap_lex 0.2.4", + "indexmap 1.9.3", "once_cell", "strsim", "termcolor", "textwrap", ] +[[package]] +name = "clap" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2686c4115cb0810d9a984776e197823d08ec94f176549a89a9efded477c456dc" +dependencies = [ + "clap_builder", + "clap_derive 4.3.2", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e53afce1efce6ed1f633cf0e57612fe51db54a1ee4fd8f8503d078fe02d69ae" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex 0.5.0", + "strsim", +] + [[package]] name = "clap_derive" version = "3.2.25" @@ -1123,6 +1261,18 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 2.0.18", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -1132,12 +1282,24 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +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" dependencies = [ "anyhow", - "clap", + "clap 3.2.25", "core-foundation", "core-services", "dirs 3.0.2", @@ -1239,15 +1401,16 @@ dependencies = [ [[package]] name = "collab" -version = "0.14.2" +version = "0.16.0" dependencies = [ "anyhow", "async-tungstenite", + "audio", "axum", "axum-extra", "base64 0.13.1", "call", - "clap", + "clap 3.2.25", "client", "collections", "ctor", @@ -1321,12 +1484,15 @@ dependencies = [ "picker", "postage", "project", + "recent_projects", "serde", "serde_derive", "settings", "theme", + "theme_selector", "util", "workspace", + "zed-actions", ] [[package]] @@ -1342,6 +1508,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "colorchoice" +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" @@ -1438,11 +1620,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" @@ -1492,6 +1680,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", + "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" @@ -1822,6 +2055,12 @@ dependencies = [ "parking_lot_core 0.9.7", ] +[[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" @@ -2131,6 +2370,12 @@ 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" @@ -2447,6 +2692,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", + "time 0.3.21", "util", ] @@ -2691,7 +2937,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" dependencies = [ "fallible-iterator", - "indexmap", + "indexmap 1.9.3", "stable_deref_trait", ] @@ -2787,7 +3033,7 @@ dependencies = [ "anyhow", "async-task", "backtrace", - "bindgen", + "bindgen 0.65.1", "block", "cc", "cocoa", @@ -2859,7 +3105,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util 0.7.8", @@ -2893,6 +3139,12 @@ dependencies = [ "ahash 0.8.3", ] +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "hashlink" version = "0.8.1" @@ -3003,6 +3255,12 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "hound" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1" + [[package]] name = "http" version = "0.2.9" @@ -3011,7 +3269,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa", + "itoa 1.0.6", ] [[package]] @@ -3070,7 +3328,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 1.0.6", "pin-project-lite 0.2.9", "socket2", "tokio", @@ -3111,11 +3369,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" dependencies = [ "android_system_properties", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows", + "windows 0.48.0", ] [[package]] @@ -3185,6 +3443,16 @@ dependencies = [ "serde", ] +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", +] + [[package]] name = "indoc" version = "1.0.9" @@ -3336,6 +3604,12 @@ dependencies = [ "either", ] +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.6" @@ -3351,6 +3625,40 @@ dependencies = [ "cc", ] +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.26" @@ -3396,12 +3704,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json_comments" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41ee439ee368ba4a77ac70d04f14015415af8600d6c894dc1f11bd79758c57d5" - [[package]] name = "jwt" version = "0.16.0" @@ -3559,6 +3861,17 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +[[package]] +name = "lewton" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777b48df9aaab155475a83a7df3070395ea1ac6902f5cd062b8f2b028075c030" +dependencies = [ + "byteorder", + "ogg", + "tinyvec", +] + [[package]] name = "libc" version = "0.2.144" @@ -3791,6 +4104,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -3847,7 +4169,7 @@ name = "media" version = "0.1.0" dependencies = [ "anyhow", - "bindgen", + "bindgen 0.65.1", "block", "bytes 1.4.0", "core-foundation", @@ -4105,6 +4427,35 @@ dependencies = [ "tempfile", ] +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + [[package]] name = "net2" version = "0.2.38" @@ -4137,6 +4488,7 @@ dependencies = [ "async-tar", "futures 0.3.28", "gpui", + "log", "parking_lot 0.11.2", "serde", "serde_derive", @@ -4212,6 +4564,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -4264,6 +4627,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "nvim-rs" version = "0.5.0" @@ -4306,7 +4690,7 @@ checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" dependencies = [ "crc32fast", "hashbrown 0.11.2", - "indexmap", + "indexmap 1.9.3", "memchr", ] @@ -4319,6 +4703,38 @@ dependencies = [ "memchr", ] +[[package]] +name = "oboe" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" +dependencies = [ + "jni 0.20.0", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" +dependencies = [ + "cc", +] + +[[package]] +name = "ogg" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +dependencies = [ + "byteorder", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -4608,7 +5024,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -4685,7 +5101,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bd9647b268a3d3e14ff09c23201133a62589c658db02bb7388c7246aafe0590" dependencies = [ "base64 0.21.0", - "indexmap", + "indexmap 1.9.3", "line-wrap", "quick-xml", "serde", @@ -4818,6 +5234,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4930,6 +5356,7 @@ dependencies = [ "language", "menu", "postage", + "pretty_assertions", "project", "schemars", "serde", @@ -5229,6 +5656,12 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "rayon" version = "1.7.0" @@ -5272,6 +5705,7 @@ version = "0.1.0" dependencies = [ "db", "editor", + "futures 0.3.28", "fuzzy", "gpui", "language", @@ -5512,6 +5946,19 @@ dependencies = [ "rmp", ] +[[package]] +name = "rodio" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" +dependencies = [ + "claxon", + "cpal", + "hound", + "lewton", + "symphonia", +] + [[package]] name = "rope" version = "0.1.0" @@ -5667,7 +6114,7 @@ dependencies = [ "bitflags", "errno 0.2.8", "io-lifetimes 0.5.3", - "itoa", + "itoa 1.0.6", "libc", "linux-raw-sys 0.0.42", "once_cell", @@ -6013,7 +6460,7 @@ checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", "security-framework-sys", ] @@ -6024,7 +6471,7 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", ] @@ -6098,8 +6545,20 @@ version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ - "indexmap", - "itoa", + "indexmap 1.9.3", + "itoa 1.0.6", + "ryu", + "serde", +] + +[[package]] +name = "serde_json_lenient" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" +dependencies = [ + "indexmap 1.9.3", + "itoa 0.4.8", "ryu", "serde", ] @@ -6122,7 +6581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.6", "ryu", "serde", ] @@ -6133,7 +6592,7 @@ version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" dependencies = [ - "indexmap", + "indexmap 1.9.3", "ryu", "serde", "yaml-rust", @@ -6148,7 +6607,7 @@ dependencies = [ "fs", "futures 0.3.28", "gpui", - "json_comments", + "indoc", "lazy_static", "postage", "pretty_assertions", @@ -6157,6 +6616,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "serde_json_lenient", "smallvec", "sqlez", "staff_mode", @@ -6506,8 +6966,8 @@ dependencies = [ "hex", "hkdf", "hmac 0.12.1", - "indexmap", - "itoa", + "indexmap 1.9.3", + "itoa 1.0.6", "libc", "libsqlite3-sys", "log", @@ -6657,6 +7117,56 @@ dependencies = [ "siphasher", ] +[[package]] +name = "symphonia" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" +dependencies = [ + "lazy_static", + "symphonia-bundle-mp3", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" +dependencies = [ + "bitflags", + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" +dependencies = [ + "arrayvec 0.7.2", + "bitflags", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + [[package]] name = "syn" version = "1.0.109" @@ -6702,7 +7212,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a902e9050fca0a5d6877550b769abd2bd1ce8c04634b941dbe2809735e1a1e33" dependencies = [ "cfg-if 1.0.0", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", "ntapi 0.4.1", "once_cell", @@ -6871,7 +7381,7 @@ dependencies = [ "anyhow", "fs", "gpui", - "indexmap", + "indexmap 1.9.3", "parking_lot 0.11.2", "schemars", "serde", @@ -6902,18 +7412,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "theme_testbench" -version = "0.1.0" -dependencies = [ - "gpui", - "project", - "settings", - "smallvec", - "theme", - "workspace", -] - [[package]] name = "thiserror" version = "1.0.40" @@ -6993,7 +7491,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ - "itoa", + "itoa 1.0.6", "serde", "time-core", "time-macros", @@ -7189,6 +7687,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.6.2" @@ -7228,7 +7743,7 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", - "indexmap", + "indexmap 1.9.3", "pin-project", "pin-project-lite 0.2.9", "rand 0.8.5", @@ -8085,7 +8600,7 @@ version = "0.85.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "570460c58b21e9150d2df0eaaedbb7816c34bcec009ae0dcc976e40ba81463e7" dependencies = [ - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -8099,7 +8614,7 @@ dependencies = [ "backtrace", "bincode", "cfg-if 1.0.0", - "indexmap", + "indexmap 1.9.3", "lazy_static", "libc", "log", @@ -8173,7 +8688,7 @@ dependencies = [ "anyhow", "cranelift-entity", "gimli 0.26.2", - "indexmap", + "indexmap 1.9.3", "log", "more-asserts", "object 0.28.4", @@ -8243,7 +8758,7 @@ dependencies = [ "backtrace", "cc", "cfg-if 1.0.0", - "indexmap", + "indexmap 1.9.3", "libc", "log", "mach", @@ -8499,6 +9014,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows" version = "0.48.0" @@ -8655,6 +9179,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.10.1" @@ -8766,6 +9299,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.3.5", + "schemars", + "serde_json", + "theme", +] + [[package]] name = "yaml-rust" version = "0.4.5" @@ -8795,7 +9339,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.92.0" +version = "0.95.0" dependencies = [ "activity_indicator", "ai", @@ -8804,6 +9348,7 @@ dependencies = [ "async-recursion 0.3.2", "async-tar", "async-trait", + "audio", "auto_update", "backtrace", "breadcrumbs", @@ -8833,7 +9378,7 @@ dependencies = [ "gpui", "ignore", "image", - "indexmap", + "indexmap 1.9.3", "install_cli", "isahc", "journal", @@ -8874,7 +9419,6 @@ dependencies = [ "text", "theme", "theme_selector", - "theme_testbench", "thiserror", "tiny_http", "toml", @@ -8906,6 +9450,14 @@ dependencies = [ "vim", "welcome", "workspace", + "zed-actions", +] + +[[package]] +name = "zed-actions" +version = "0.1.0" +dependencies = [ + "gpui", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fca7355964..1708ccfc0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/activity_indicator", "crates/ai", + "crates/audio", "crates/auto_update", "crates/breadcrumbs", "crates/call", @@ -61,12 +62,13 @@ members = [ "crates/text", "crates/theme", "crates/theme_selector", - "crates/theme_testbench", "crates/util", "crates/vim", "crates/workspace", "crates/welcome", + "crates/xtask", "crates/zed", + "crates/zed-actions" ] default-members = ["crates/zed"] resolver = "2" @@ -100,6 +102,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" } @@ -118,3 +121,4 @@ split-debuginfo = "unpacked" [profile.release] debug = true lto = "thin" +codegen-units = 1 diff --git a/assets/icons/assist_15.svg b/assets/icons/assist_15.svg new file mode 100644 index 0000000000..3baf8df3e9 --- /dev/null +++ b/assets/icons/assist_15.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/hamburger_15.svg b/assets/icons/hamburger_15.svg new file mode 100644 index 0000000000..060caeecbf --- /dev/null +++ b/assets/icons/hamburger_15.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/quote_15.svg b/assets/icons/quote_15.svg new file mode 100644 index 0000000000..be5eabd9b0 --- /dev/null +++ b/assets/icons/quote_15.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/radix/accessibility.svg b/assets/icons/radix/accessibility.svg new file mode 100644 index 0000000000..32d78f2d8d --- /dev/null +++ b/assets/icons/radix/accessibility.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/activity-log.svg b/assets/icons/radix/activity-log.svg new file mode 100644 index 0000000000..8feab7d449 --- /dev/null +++ b/assets/icons/radix/activity-log.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-baseline.svg b/assets/icons/radix/align-baseline.svg new file mode 100644 index 0000000000..07213dc1ae --- /dev/null +++ b/assets/icons/radix/align-baseline.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-bottom.svg b/assets/icons/radix/align-bottom.svg new file mode 100644 index 0000000000..7d11c0cd5a --- /dev/null +++ b/assets/icons/radix/align-bottom.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-center-horizontally.svg b/assets/icons/radix/align-center-horizontally.svg new file mode 100644 index 0000000000..69509a7d09 --- /dev/null +++ b/assets/icons/radix/align-center-horizontally.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-center-vertically.svg b/assets/icons/radix/align-center-vertically.svg new file mode 100644 index 0000000000..4f1b50cc43 --- /dev/null +++ b/assets/icons/radix/align-center-vertically.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-center.svg b/assets/icons/radix/align-center.svg new file mode 100644 index 0000000000..caaec36477 --- /dev/null +++ b/assets/icons/radix/align-center.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-end.svg b/assets/icons/radix/align-end.svg new file mode 100644 index 0000000000..18f1b64912 --- /dev/null +++ b/assets/icons/radix/align-end.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-horizontal-centers.svg b/assets/icons/radix/align-horizontal-centers.svg new file mode 100644 index 0000000000..2d1d64ea4b --- /dev/null +++ b/assets/icons/radix/align-horizontal-centers.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-left.svg b/assets/icons/radix/align-left.svg new file mode 100644 index 0000000000..0d5dba095c --- /dev/null +++ b/assets/icons/radix/align-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-right.svg b/assets/icons/radix/align-right.svg new file mode 100644 index 0000000000..1b6b3f0ffa --- /dev/null +++ b/assets/icons/radix/align-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-start.svg b/assets/icons/radix/align-start.svg new file mode 100644 index 0000000000..ada50e1079 --- /dev/null +++ b/assets/icons/radix/align-start.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-stretch.svg b/assets/icons/radix/align-stretch.svg new file mode 100644 index 0000000000..3cb28605cb --- /dev/null +++ b/assets/icons/radix/align-stretch.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-top.svg b/assets/icons/radix/align-top.svg new file mode 100644 index 0000000000..23db80f4dd --- /dev/null +++ b/assets/icons/radix/align-top.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/align-vertical-centers.svg b/assets/icons/radix/align-vertical-centers.svg new file mode 100644 index 0000000000..07eaee7bf7 --- /dev/null +++ b/assets/icons/radix/align-vertical-centers.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/all-sides.svg b/assets/icons/radix/all-sides.svg new file mode 100644 index 0000000000..8ace7df03f --- /dev/null +++ b/assets/icons/radix/all-sides.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/angle.svg b/assets/icons/radix/angle.svg new file mode 100644 index 0000000000..a0d93f3460 --- /dev/null +++ b/assets/icons/radix/angle.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/archive.svg b/assets/icons/radix/archive.svg new file mode 100644 index 0000000000..74063f1d1e --- /dev/null +++ b/assets/icons/radix/archive.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-bottom-left.svg b/assets/icons/radix/arrow-bottom-left.svg new file mode 100644 index 0000000000..7a4511aa2d --- /dev/null +++ b/assets/icons/radix/arrow-bottom-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-bottom-right.svg b/assets/icons/radix/arrow-bottom-right.svg new file mode 100644 index 0000000000..2ba9fef101 --- /dev/null +++ b/assets/icons/radix/arrow-bottom-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-down.svg b/assets/icons/radix/arrow-down.svg new file mode 100644 index 0000000000..5dc21a6689 --- /dev/null +++ b/assets/icons/radix/arrow-down.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-left.svg b/assets/icons/radix/arrow-left.svg new file mode 100644 index 0000000000..3a64c8394f --- /dev/null +++ b/assets/icons/radix/arrow-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-right.svg b/assets/icons/radix/arrow-right.svg new file mode 100644 index 0000000000..e3d30988d5 --- /dev/null +++ b/assets/icons/radix/arrow-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-top-left.svg b/assets/icons/radix/arrow-top-left.svg new file mode 100644 index 0000000000..69fef41dee --- /dev/null +++ b/assets/icons/radix/arrow-top-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-top-right.svg b/assets/icons/radix/arrow-top-right.svg new file mode 100644 index 0000000000..c1016376e3 --- /dev/null +++ b/assets/icons/radix/arrow-top-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/arrow-up.svg b/assets/icons/radix/arrow-up.svg new file mode 100644 index 0000000000..ba426119e9 --- /dev/null +++ b/assets/icons/radix/arrow-up.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/aspect-ratio.svg b/assets/icons/radix/aspect-ratio.svg new file mode 100644 index 0000000000..0851f2e1e9 --- /dev/null +++ b/assets/icons/radix/aspect-ratio.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/avatar.svg b/assets/icons/radix/avatar.svg new file mode 100644 index 0000000000..cb229c77fe --- /dev/null +++ b/assets/icons/radix/avatar.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/backpack.svg b/assets/icons/radix/backpack.svg new file mode 100644 index 0000000000..a5c9cedbd3 --- /dev/null +++ b/assets/icons/radix/backpack.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/badge.svg b/assets/icons/radix/badge.svg new file mode 100644 index 0000000000..aa764d4726 --- /dev/null +++ b/assets/icons/radix/badge.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/bar-chart.svg b/assets/icons/radix/bar-chart.svg new file mode 100644 index 0000000000..f8054781d9 --- /dev/null +++ b/assets/icons/radix/bar-chart.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/bell.svg b/assets/icons/radix/bell.svg new file mode 100644 index 0000000000..ea1c6dd42e --- /dev/null +++ b/assets/icons/radix/bell.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/blending-mode.svg b/assets/icons/radix/blending-mode.svg new file mode 100644 index 0000000000..bd58cf4ee3 --- /dev/null +++ b/assets/icons/radix/blending-mode.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/bookmark-filled.svg b/assets/icons/radix/bookmark-filled.svg new file mode 100644 index 0000000000..5b725cd88d --- /dev/null +++ b/assets/icons/radix/bookmark-filled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/bookmark.svg b/assets/icons/radix/bookmark.svg new file mode 100644 index 0000000000..90c4d827f1 --- /dev/null +++ b/assets/icons/radix/bookmark.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/border-all.svg b/assets/icons/radix/border-all.svg new file mode 100644 index 0000000000..3bfde7d59b --- /dev/null +++ b/assets/icons/radix/border-all.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/assets/icons/radix/border-bottom.svg b/assets/icons/radix/border-bottom.svg new file mode 100644 index 0000000000..f2d3c3d554 --- /dev/null +++ b/assets/icons/radix/border-bottom.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/radix/border-dashed.svg b/assets/icons/radix/border-dashed.svg new file mode 100644 index 0000000000..85fdcdfe5d --- /dev/null +++ b/assets/icons/radix/border-dashed.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/border-dotted.svg b/assets/icons/radix/border-dotted.svg new file mode 100644 index 0000000000..5eb514ed2a --- /dev/null +++ b/assets/icons/radix/border-dotted.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/border-left.svg b/assets/icons/radix/border-left.svg new file mode 100644 index 0000000000..5deb197da5 --- /dev/null +++ b/assets/icons/radix/border-left.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/radix/border-none.svg b/assets/icons/radix/border-none.svg new file mode 100644 index 0000000000..1ad3f59d7c --- /dev/null +++ b/assets/icons/radix/border-none.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/radix/border-right.svg b/assets/icons/radix/border-right.svg new file mode 100644 index 0000000000..c939095ad7 --- /dev/null +++ b/assets/icons/radix/border-right.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/radix/border-solid.svg b/assets/icons/radix/border-solid.svg new file mode 100644 index 0000000000..5c0d26a058 --- /dev/null +++ b/assets/icons/radix/border-solid.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/border-split.svg b/assets/icons/radix/border-split.svg new file mode 100644 index 0000000000..7fdf6cc34e --- /dev/null +++ b/assets/icons/radix/border-split.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + diff --git a/assets/icons/radix/border-style.svg b/assets/icons/radix/border-style.svg new file mode 100644 index 0000000000..f729cb993b --- /dev/null +++ b/assets/icons/radix/border-style.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/border-top.svg b/assets/icons/radix/border-top.svg new file mode 100644 index 0000000000..bde739d755 --- /dev/null +++ b/assets/icons/radix/border-top.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/radix/border-width.svg b/assets/icons/radix/border-width.svg new file mode 100644 index 0000000000..37c270756e --- /dev/null +++ b/assets/icons/radix/border-width.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/box-model.svg b/assets/icons/radix/box-model.svg new file mode 100644 index 0000000000..45d1a7ce41 --- /dev/null +++ b/assets/icons/radix/box-model.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/box.svg b/assets/icons/radix/box.svg new file mode 100644 index 0000000000..6e035c21ed --- /dev/null +++ b/assets/icons/radix/box.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/button.svg b/assets/icons/radix/button.svg new file mode 100644 index 0000000000..31622bcf15 --- /dev/null +++ b/assets/icons/radix/button.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/calendar.svg b/assets/icons/radix/calendar.svg new file mode 100644 index 0000000000..2adbe0bc28 --- /dev/null +++ b/assets/icons/radix/calendar.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/camera.svg b/assets/icons/radix/camera.svg new file mode 100644 index 0000000000..d7cccf74c2 --- /dev/null +++ b/assets/icons/radix/camera.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/card-stack-minus.svg b/assets/icons/radix/card-stack-minus.svg new file mode 100644 index 0000000000..04d8e51178 --- /dev/null +++ b/assets/icons/radix/card-stack-minus.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/card-stack-plus.svg b/assets/icons/radix/card-stack-plus.svg new file mode 100644 index 0000000000..a184f4bc1a --- /dev/null +++ b/assets/icons/radix/card-stack-plus.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/card-stack.svg b/assets/icons/radix/card-stack.svg new file mode 100644 index 0000000000..defea0e165 --- /dev/null +++ b/assets/icons/radix/card-stack.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/caret-down.svg b/assets/icons/radix/caret-down.svg new file mode 100644 index 0000000000..ff8b8c3b88 --- /dev/null +++ b/assets/icons/radix/caret-down.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/caret-left.svg b/assets/icons/radix/caret-left.svg new file mode 100644 index 0000000000..969bc3b95c --- /dev/null +++ b/assets/icons/radix/caret-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/caret-right.svg b/assets/icons/radix/caret-right.svg new file mode 100644 index 0000000000..75c55d8676 --- /dev/null +++ b/assets/icons/radix/caret-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/caret-sort.svg b/assets/icons/radix/caret-sort.svg new file mode 100644 index 0000000000..a65e20b660 --- /dev/null +++ b/assets/icons/radix/caret-sort.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/caret-up.svg b/assets/icons/radix/caret-up.svg new file mode 100644 index 0000000000..53026b83d8 --- /dev/null +++ b/assets/icons/radix/caret-up.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/chat-bubble.svg b/assets/icons/radix/chat-bubble.svg new file mode 100644 index 0000000000..5766f46de8 --- /dev/null +++ b/assets/icons/radix/chat-bubble.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/check-circled.svg b/assets/icons/radix/check-circled.svg new file mode 100644 index 0000000000..19ee22eb51 --- /dev/null +++ b/assets/icons/radix/check-circled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/check.svg b/assets/icons/radix/check.svg new file mode 100644 index 0000000000..476a3baa18 --- /dev/null +++ b/assets/icons/radix/check.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/checkbox.svg b/assets/icons/radix/checkbox.svg new file mode 100644 index 0000000000..d6bb3c7ef2 --- /dev/null +++ b/assets/icons/radix/checkbox.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/chevron-down.svg b/assets/icons/radix/chevron-down.svg new file mode 100644 index 0000000000..175c1312fd --- /dev/null +++ b/assets/icons/radix/chevron-down.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/chevron-left.svg b/assets/icons/radix/chevron-left.svg new file mode 100644 index 0000000000..d7628202f2 --- /dev/null +++ b/assets/icons/radix/chevron-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/chevron-right.svg b/assets/icons/radix/chevron-right.svg new file mode 100644 index 0000000000..e3ebd73d99 --- /dev/null +++ b/assets/icons/radix/chevron-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/chevron-up.svg b/assets/icons/radix/chevron-up.svg new file mode 100644 index 0000000000..0e8e796dab --- /dev/null +++ b/assets/icons/radix/chevron-up.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/circle-backslash.svg b/assets/icons/radix/circle-backslash.svg new file mode 100644 index 0000000000..40c4dd5398 --- /dev/null +++ b/assets/icons/radix/circle-backslash.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/circle.svg b/assets/icons/radix/circle.svg new file mode 100644 index 0000000000..ba4a8f22fe --- /dev/null +++ b/assets/icons/radix/circle.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/clipboard-copy.svg b/assets/icons/radix/clipboard-copy.svg new file mode 100644 index 0000000000..5293fdc493 --- /dev/null +++ b/assets/icons/radix/clipboard-copy.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/clipboard.svg b/assets/icons/radix/clipboard.svg new file mode 100644 index 0000000000..e18b32943b --- /dev/null +++ b/assets/icons/radix/clipboard.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/clock.svg b/assets/icons/radix/clock.svg new file mode 100644 index 0000000000..ac3b526fbb --- /dev/null +++ b/assets/icons/radix/clock.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/code.svg b/assets/icons/radix/code.svg new file mode 100644 index 0000000000..70fe381b68 --- /dev/null +++ b/assets/icons/radix/code.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/codesandbox-logo.svg b/assets/icons/radix/codesandbox-logo.svg new file mode 100644 index 0000000000..4a3f549c2f --- /dev/null +++ b/assets/icons/radix/codesandbox-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/color-wheel.svg b/assets/icons/radix/color-wheel.svg new file mode 100644 index 0000000000..2153b84428 --- /dev/null +++ b/assets/icons/radix/color-wheel.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/column-spacing.svg b/assets/icons/radix/column-spacing.svg new file mode 100644 index 0000000000..aafcf555cb --- /dev/null +++ b/assets/icons/radix/column-spacing.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/columns.svg b/assets/icons/radix/columns.svg new file mode 100644 index 0000000000..e1607611b1 --- /dev/null +++ b/assets/icons/radix/columns.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/commit.svg b/assets/icons/radix/commit.svg new file mode 100644 index 0000000000..ac128a2b08 --- /dev/null +++ b/assets/icons/radix/commit.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/component-1.svg b/assets/icons/radix/component-1.svg new file mode 100644 index 0000000000..e3e9f38af1 --- /dev/null +++ b/assets/icons/radix/component-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/component-2.svg b/assets/icons/radix/component-2.svg new file mode 100644 index 0000000000..df2091d143 --- /dev/null +++ b/assets/icons/radix/component-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/component-boolean.svg b/assets/icons/radix/component-boolean.svg new file mode 100644 index 0000000000..942e8832eb --- /dev/null +++ b/assets/icons/radix/component-boolean.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/component-instance.svg b/assets/icons/radix/component-instance.svg new file mode 100644 index 0000000000..048c401291 --- /dev/null +++ b/assets/icons/radix/component-instance.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/component-none.svg b/assets/icons/radix/component-none.svg new file mode 100644 index 0000000000..a622c3ee96 --- /dev/null +++ b/assets/icons/radix/component-none.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/component-placeholder.svg b/assets/icons/radix/component-placeholder.svg new file mode 100644 index 0000000000..b8892d5d23 --- /dev/null +++ b/assets/icons/radix/component-placeholder.svg @@ -0,0 +1,12 @@ + + + + diff --git a/assets/icons/radix/container.svg b/assets/icons/radix/container.svg new file mode 100644 index 0000000000..1c2a4fd0e1 --- /dev/null +++ b/assets/icons/radix/container.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/cookie.svg b/assets/icons/radix/cookie.svg new file mode 100644 index 0000000000..8c165601a2 --- /dev/null +++ b/assets/icons/radix/cookie.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/copy.svg b/assets/icons/radix/copy.svg new file mode 100644 index 0000000000..bf2b504ecf --- /dev/null +++ b/assets/icons/radix/copy.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/corner-bottom-left.svg b/assets/icons/radix/corner-bottom-left.svg new file mode 100644 index 0000000000..26df9dbad8 --- /dev/null +++ b/assets/icons/radix/corner-bottom-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/corner-bottom-right.svg b/assets/icons/radix/corner-bottom-right.svg new file mode 100644 index 0000000000..15e3957123 --- /dev/null +++ b/assets/icons/radix/corner-bottom-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/corner-top-left.svg b/assets/icons/radix/corner-top-left.svg new file mode 100644 index 0000000000..8fc1b84b82 --- /dev/null +++ b/assets/icons/radix/corner-top-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/corner-top-right.svg b/assets/icons/radix/corner-top-right.svg new file mode 100644 index 0000000000..533ea6c678 --- /dev/null +++ b/assets/icons/radix/corner-top-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/corners.svg b/assets/icons/radix/corners.svg new file mode 100644 index 0000000000..c41c4e0183 --- /dev/null +++ b/assets/icons/radix/corners.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/countdown-timer.svg b/assets/icons/radix/countdown-timer.svg new file mode 100644 index 0000000000..58494bd416 --- /dev/null +++ b/assets/icons/radix/countdown-timer.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/counter-clockwise-clock.svg b/assets/icons/radix/counter-clockwise-clock.svg new file mode 100644 index 0000000000..0b3acbcebf --- /dev/null +++ b/assets/icons/radix/counter-clockwise-clock.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/crop.svg b/assets/icons/radix/crop.svg new file mode 100644 index 0000000000..008457fff6 --- /dev/null +++ b/assets/icons/radix/crop.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/cross-1.svg b/assets/icons/radix/cross-1.svg new file mode 100644 index 0000000000..62135d27ed --- /dev/null +++ b/assets/icons/radix/cross-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/cross-2.svg b/assets/icons/radix/cross-2.svg new file mode 100644 index 0000000000..4c55700928 --- /dev/null +++ b/assets/icons/radix/cross-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/cross-circled.svg b/assets/icons/radix/cross-circled.svg new file mode 100644 index 0000000000..df3cb896c8 --- /dev/null +++ b/assets/icons/radix/cross-circled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/crosshair-1.svg b/assets/icons/radix/crosshair-1.svg new file mode 100644 index 0000000000..05b22f8461 --- /dev/null +++ b/assets/icons/radix/crosshair-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/crosshair-2.svg b/assets/icons/radix/crosshair-2.svg new file mode 100644 index 0000000000..f5ee0a92af --- /dev/null +++ b/assets/icons/radix/crosshair-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/crumpled-paper.svg b/assets/icons/radix/crumpled-paper.svg new file mode 100644 index 0000000000..33e9b65581 --- /dev/null +++ b/assets/icons/radix/crumpled-paper.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/cube.svg b/assets/icons/radix/cube.svg new file mode 100644 index 0000000000..b327158be4 --- /dev/null +++ b/assets/icons/radix/cube.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/cursor-arrow.svg b/assets/icons/radix/cursor-arrow.svg new file mode 100644 index 0000000000..b0227e4ded --- /dev/null +++ b/assets/icons/radix/cursor-arrow.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/cursor-text.svg b/assets/icons/radix/cursor-text.svg new file mode 100644 index 0000000000..05939503b8 --- /dev/null +++ b/assets/icons/radix/cursor-text.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/dash.svg b/assets/icons/radix/dash.svg new file mode 100644 index 0000000000..d70daf7fed --- /dev/null +++ b/assets/icons/radix/dash.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/dashboard.svg b/assets/icons/radix/dashboard.svg new file mode 100644 index 0000000000..38008c64e4 --- /dev/null +++ b/assets/icons/radix/dashboard.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/desktop-mute.svg b/assets/icons/radix/desktop-mute.svg new file mode 100644 index 0000000000..83d249176f --- /dev/null +++ b/assets/icons/radix/desktop-mute.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/radix/desktop.svg b/assets/icons/radix/desktop.svg new file mode 100644 index 0000000000..ad252e64cf --- /dev/null +++ b/assets/icons/radix/desktop.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/dimensions.svg b/assets/icons/radix/dimensions.svg new file mode 100644 index 0000000000..767d1d2896 --- /dev/null +++ b/assets/icons/radix/dimensions.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/disc.svg b/assets/icons/radix/disc.svg new file mode 100644 index 0000000000..6e19caab35 --- /dev/null +++ b/assets/icons/radix/disc.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/discord-logo.svg b/assets/icons/radix/discord-logo.svg new file mode 100644 index 0000000000..50567c212e --- /dev/null +++ b/assets/icons/radix/discord-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/assets/icons/radix/divider-horizontal.svg b/assets/icons/radix/divider-horizontal.svg new file mode 100644 index 0000000000..59e43649c9 --- /dev/null +++ b/assets/icons/radix/divider-horizontal.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/divider-vertical.svg b/assets/icons/radix/divider-vertical.svg new file mode 100644 index 0000000000..95f5cc8f2f --- /dev/null +++ b/assets/icons/radix/divider-vertical.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/dot-filled.svg b/assets/icons/radix/dot-filled.svg new file mode 100644 index 0000000000..0c1a17b3bd --- /dev/null +++ b/assets/icons/radix/dot-filled.svg @@ -0,0 +1,6 @@ + + + diff --git a/assets/icons/radix/dot-solid.svg b/assets/icons/radix/dot-solid.svg new file mode 100644 index 0000000000..0c1a17b3bd --- /dev/null +++ b/assets/icons/radix/dot-solid.svg @@ -0,0 +1,6 @@ + + + diff --git a/assets/icons/radix/dot.svg b/assets/icons/radix/dot.svg new file mode 100644 index 0000000000..c553a1422d --- /dev/null +++ b/assets/icons/radix/dot.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/dots-horizontal.svg b/assets/icons/radix/dots-horizontal.svg new file mode 100644 index 0000000000..347d1ae13d --- /dev/null +++ b/assets/icons/radix/dots-horizontal.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/dots-vertical.svg b/assets/icons/radix/dots-vertical.svg new file mode 100644 index 0000000000..5ca1a181e3 --- /dev/null +++ b/assets/icons/radix/dots-vertical.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/double-arrow-down.svg b/assets/icons/radix/double-arrow-down.svg new file mode 100644 index 0000000000..8b86db2f8a --- /dev/null +++ b/assets/icons/radix/double-arrow-down.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/double-arrow-left.svg b/assets/icons/radix/double-arrow-left.svg new file mode 100644 index 0000000000..0ef30ff955 --- /dev/null +++ b/assets/icons/radix/double-arrow-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/double-arrow-right.svg b/assets/icons/radix/double-arrow-right.svg new file mode 100644 index 0000000000..9997fdc403 --- /dev/null +++ b/assets/icons/radix/double-arrow-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/double-arrow-up.svg b/assets/icons/radix/double-arrow-up.svg new file mode 100644 index 0000000000..8d571fcd66 --- /dev/null +++ b/assets/icons/radix/double-arrow-up.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/download.svg b/assets/icons/radix/download.svg new file mode 100644 index 0000000000..49a05d5f47 --- /dev/null +++ b/assets/icons/radix/download.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/drag-handle-dots-1.svg b/assets/icons/radix/drag-handle-dots-1.svg new file mode 100644 index 0000000000..fc046bb9d9 --- /dev/null +++ b/assets/icons/radix/drag-handle-dots-1.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/radix/drag-handle-dots-2.svg b/assets/icons/radix/drag-handle-dots-2.svg new file mode 100644 index 0000000000..aed0e702d7 --- /dev/null +++ b/assets/icons/radix/drag-handle-dots-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/drag-handle-horizontal.svg b/assets/icons/radix/drag-handle-horizontal.svg new file mode 100644 index 0000000000..c1bb138a24 --- /dev/null +++ b/assets/icons/radix/drag-handle-horizontal.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/drag-handle-vertical.svg b/assets/icons/radix/drag-handle-vertical.svg new file mode 100644 index 0000000000..8d48c7894a --- /dev/null +++ b/assets/icons/radix/drag-handle-vertical.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/drawing-pin-filled.svg b/assets/icons/radix/drawing-pin-filled.svg new file mode 100644 index 0000000000..e1894619c3 --- /dev/null +++ b/assets/icons/radix/drawing-pin-filled.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/radix/drawing-pin-solid.svg b/assets/icons/radix/drawing-pin-solid.svg new file mode 100644 index 0000000000..e1894619c3 --- /dev/null +++ b/assets/icons/radix/drawing-pin-solid.svg @@ -0,0 +1,14 @@ + + + + diff --git a/assets/icons/radix/drawing-pin.svg b/assets/icons/radix/drawing-pin.svg new file mode 100644 index 0000000000..5625e7588f --- /dev/null +++ b/assets/icons/radix/drawing-pin.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/dropdown-menu.svg b/assets/icons/radix/dropdown-menu.svg new file mode 100644 index 0000000000..c938052be8 --- /dev/null +++ b/assets/icons/radix/dropdown-menu.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/enter-full-screen.svg b/assets/icons/radix/enter-full-screen.svg new file mode 100644 index 0000000000..d368a6d415 --- /dev/null +++ b/assets/icons/radix/enter-full-screen.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/enter.svg b/assets/icons/radix/enter.svg new file mode 100644 index 0000000000..cc57d74cea --- /dev/null +++ b/assets/icons/radix/enter.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/envelope-closed.svg b/assets/icons/radix/envelope-closed.svg new file mode 100644 index 0000000000..4b5e037840 --- /dev/null +++ b/assets/icons/radix/envelope-closed.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/envelope-open.svg b/assets/icons/radix/envelope-open.svg new file mode 100644 index 0000000000..df1e3fea95 --- /dev/null +++ b/assets/icons/radix/envelope-open.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/eraser.svg b/assets/icons/radix/eraser.svg new file mode 100644 index 0000000000..bb448d4d23 --- /dev/null +++ b/assets/icons/radix/eraser.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/exclamation-triangle.svg b/assets/icons/radix/exclamation-triangle.svg new file mode 100644 index 0000000000..210d4c45c6 --- /dev/null +++ b/assets/icons/radix/exclamation-triangle.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/exit-full-screen.svg b/assets/icons/radix/exit-full-screen.svg new file mode 100644 index 0000000000..9b6439b043 --- /dev/null +++ b/assets/icons/radix/exit-full-screen.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/exit.svg b/assets/icons/radix/exit.svg new file mode 100644 index 0000000000..2cc6ce120d --- /dev/null +++ b/assets/icons/radix/exit.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/external-link.svg b/assets/icons/radix/external-link.svg new file mode 100644 index 0000000000..0ee7420162 --- /dev/null +++ b/assets/icons/radix/external-link.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/eye-closed.svg b/assets/icons/radix/eye-closed.svg new file mode 100644 index 0000000000..f824fe55f9 --- /dev/null +++ b/assets/icons/radix/eye-closed.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/eye-none.svg b/assets/icons/radix/eye-none.svg new file mode 100644 index 0000000000..d4beecd33a --- /dev/null +++ b/assets/icons/radix/eye-none.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/eye-open.svg b/assets/icons/radix/eye-open.svg new file mode 100644 index 0000000000..d39d26b2c1 --- /dev/null +++ b/assets/icons/radix/eye-open.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/face.svg b/assets/icons/radix/face.svg new file mode 100644 index 0000000000..81b14dd8d7 --- /dev/null +++ b/assets/icons/radix/face.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/figma-logo.svg b/assets/icons/radix/figma-logo.svg new file mode 100644 index 0000000000..6c19276554 --- /dev/null +++ b/assets/icons/radix/figma-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/file-minus.svg b/assets/icons/radix/file-minus.svg new file mode 100644 index 0000000000..bd1a841881 --- /dev/null +++ b/assets/icons/radix/file-minus.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/file-plus.svg b/assets/icons/radix/file-plus.svg new file mode 100644 index 0000000000..2396e20015 --- /dev/null +++ b/assets/icons/radix/file-plus.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/file-text.svg b/assets/icons/radix/file-text.svg new file mode 100644 index 0000000000..f341ab8abf --- /dev/null +++ b/assets/icons/radix/file-text.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/file.svg b/assets/icons/radix/file.svg new file mode 100644 index 0000000000..5f256b42e1 --- /dev/null +++ b/assets/icons/radix/file.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/font-bold.svg b/assets/icons/radix/font-bold.svg new file mode 100644 index 0000000000..7dc6caf3b0 --- /dev/null +++ b/assets/icons/radix/font-bold.svg @@ -0,0 +1,6 @@ + + + diff --git a/assets/icons/radix/font-family.svg b/assets/icons/radix/font-family.svg new file mode 100644 index 0000000000..9134b9086d --- /dev/null +++ b/assets/icons/radix/font-family.svg @@ -0,0 +1,6 @@ + + + diff --git a/assets/icons/radix/font-italic.svg b/assets/icons/radix/font-italic.svg new file mode 100644 index 0000000000..6e6288d6bc --- /dev/null +++ b/assets/icons/radix/font-italic.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/font-roman.svg b/assets/icons/radix/font-roman.svg new file mode 100644 index 0000000000..c595b790fc --- /dev/null +++ b/assets/icons/radix/font-roman.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/font-size.svg b/assets/icons/radix/font-size.svg new file mode 100644 index 0000000000..e389a58d73 --- /dev/null +++ b/assets/icons/radix/font-size.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/font-style.svg b/assets/icons/radix/font-style.svg new file mode 100644 index 0000000000..31c3730130 --- /dev/null +++ b/assets/icons/radix/font-style.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/frame.svg b/assets/icons/radix/frame.svg new file mode 100644 index 0000000000..ec61a48efa --- /dev/null +++ b/assets/icons/radix/frame.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/framer-logo.svg b/assets/icons/radix/framer-logo.svg new file mode 100644 index 0000000000..68be3b317b --- /dev/null +++ b/assets/icons/radix/framer-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/gear.svg b/assets/icons/radix/gear.svg new file mode 100644 index 0000000000..52f9e17312 --- /dev/null +++ b/assets/icons/radix/gear.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/github-logo.svg b/assets/icons/radix/github-logo.svg new file mode 100644 index 0000000000..e46612cf56 --- /dev/null +++ b/assets/icons/radix/github-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/globe.svg b/assets/icons/radix/globe.svg new file mode 100644 index 0000000000..4728b827df --- /dev/null +++ b/assets/icons/radix/globe.svg @@ -0,0 +1,26 @@ + + + + + + diff --git a/assets/icons/radix/grid.svg b/assets/icons/radix/grid.svg new file mode 100644 index 0000000000..5d9af33572 --- /dev/null +++ b/assets/icons/radix/grid.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/group.svg b/assets/icons/radix/group.svg new file mode 100644 index 0000000000..c3c91d211f --- /dev/null +++ b/assets/icons/radix/group.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/half-1.svg b/assets/icons/radix/half-1.svg new file mode 100644 index 0000000000..9890e26bb8 --- /dev/null +++ b/assets/icons/radix/half-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/half-2.svg b/assets/icons/radix/half-2.svg new file mode 100644 index 0000000000..4db1d564cb --- /dev/null +++ b/assets/icons/radix/half-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/hamburger-menu.svg b/assets/icons/radix/hamburger-menu.svg new file mode 100644 index 0000000000..039168055b --- /dev/null +++ b/assets/icons/radix/hamburger-menu.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/hand.svg b/assets/icons/radix/hand.svg new file mode 100644 index 0000000000..12afac8f5f --- /dev/null +++ b/assets/icons/radix/hand.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/heading.svg b/assets/icons/radix/heading.svg new file mode 100644 index 0000000000..0a5e2caaf1 --- /dev/null +++ b/assets/icons/radix/heading.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/heart-filled.svg b/assets/icons/radix/heart-filled.svg new file mode 100644 index 0000000000..94928accd7 --- /dev/null +++ b/assets/icons/radix/heart-filled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/heart.svg b/assets/icons/radix/heart.svg new file mode 100644 index 0000000000..91cbc450fd --- /dev/null +++ b/assets/icons/radix/heart.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/height.svg b/assets/icons/radix/height.svg new file mode 100644 index 0000000000..28424f4d51 --- /dev/null +++ b/assets/icons/radix/height.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/hobby-knife.svg b/assets/icons/radix/hobby-knife.svg new file mode 100644 index 0000000000..c2ed3fb1ed --- /dev/null +++ b/assets/icons/radix/hobby-knife.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/home.svg b/assets/icons/radix/home.svg new file mode 100644 index 0000000000..733bd79113 --- /dev/null +++ b/assets/icons/radix/home.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/iconjar-logo.svg b/assets/icons/radix/iconjar-logo.svg new file mode 100644 index 0000000000..c154b4e864 --- /dev/null +++ b/assets/icons/radix/iconjar-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/id-card.svg b/assets/icons/radix/id-card.svg new file mode 100644 index 0000000000..efde9ffa7e --- /dev/null +++ b/assets/icons/radix/id-card.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/image.svg b/assets/icons/radix/image.svg new file mode 100644 index 0000000000..0ff4475252 --- /dev/null +++ b/assets/icons/radix/image.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/info-circled.svg b/assets/icons/radix/info-circled.svg new file mode 100644 index 0000000000..4ab1b260e3 --- /dev/null +++ b/assets/icons/radix/info-circled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/inner-shadow.svg b/assets/icons/radix/inner-shadow.svg new file mode 100644 index 0000000000..1056a7bffc --- /dev/null +++ b/assets/icons/radix/inner-shadow.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + diff --git a/assets/icons/radix/input.svg b/assets/icons/radix/input.svg new file mode 100644 index 0000000000..4ed4605b2c --- /dev/null +++ b/assets/icons/radix/input.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/instagram-logo.svg b/assets/icons/radix/instagram-logo.svg new file mode 100644 index 0000000000..5d78937966 --- /dev/null +++ b/assets/icons/radix/instagram-logo.svg @@ -0,0 +1,8 @@ + + + + + diff --git a/assets/icons/radix/justify-center.svg b/assets/icons/radix/justify-center.svg new file mode 100644 index 0000000000..7999a4ea46 --- /dev/null +++ b/assets/icons/radix/justify-center.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/justify-end.svg b/assets/icons/radix/justify-end.svg new file mode 100644 index 0000000000..bb52f493d7 --- /dev/null +++ b/assets/icons/radix/justify-end.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/justify-start.svg b/assets/icons/radix/justify-start.svg new file mode 100644 index 0000000000..648ca0b603 --- /dev/null +++ b/assets/icons/radix/justify-start.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/justify-stretch.svg b/assets/icons/radix/justify-stretch.svg new file mode 100644 index 0000000000..83df0a8959 --- /dev/null +++ b/assets/icons/radix/justify-stretch.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/keyboard.svg b/assets/icons/radix/keyboard.svg new file mode 100644 index 0000000000..fc6f86bfc2 --- /dev/null +++ b/assets/icons/radix/keyboard.svg @@ -0,0 +1,7 @@ + + + + diff --git a/assets/icons/radix/lap-timer.svg b/assets/icons/radix/lap-timer.svg new file mode 100644 index 0000000000..1de0b3be6c --- /dev/null +++ b/assets/icons/radix/lap-timer.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/laptop.svg b/assets/icons/radix/laptop.svg new file mode 100644 index 0000000000..6aff5d6d44 --- /dev/null +++ b/assets/icons/radix/laptop.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/layers.svg b/assets/icons/radix/layers.svg new file mode 100644 index 0000000000..821993fc70 --- /dev/null +++ b/assets/icons/radix/layers.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/layout.svg b/assets/icons/radix/layout.svg new file mode 100644 index 0000000000..8e4a352f50 --- /dev/null +++ b/assets/icons/radix/layout.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/letter-case-capitalize.svg b/assets/icons/radix/letter-case-capitalize.svg new file mode 100644 index 0000000000..16617ecf7e --- /dev/null +++ b/assets/icons/radix/letter-case-capitalize.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/letter-case-lowercase.svg b/assets/icons/radix/letter-case-lowercase.svg new file mode 100644 index 0000000000..61aefb9aad --- /dev/null +++ b/assets/icons/radix/letter-case-lowercase.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/letter-case-toggle.svg b/assets/icons/radix/letter-case-toggle.svg new file mode 100644 index 0000000000..a021a2b922 --- /dev/null +++ b/assets/icons/radix/letter-case-toggle.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/letter-case-uppercase.svg b/assets/icons/radix/letter-case-uppercase.svg new file mode 100644 index 0000000000..ccd2be04e7 --- /dev/null +++ b/assets/icons/radix/letter-case-uppercase.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/letter-spacing.svg b/assets/icons/radix/letter-spacing.svg new file mode 100644 index 0000000000..073023e0f4 --- /dev/null +++ b/assets/icons/radix/letter-spacing.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/lightning-bolt.svg b/assets/icons/radix/lightning-bolt.svg new file mode 100644 index 0000000000..7c35df9cfe --- /dev/null +++ b/assets/icons/radix/lightning-bolt.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/line-height.svg b/assets/icons/radix/line-height.svg new file mode 100644 index 0000000000..1c302d1ffc --- /dev/null +++ b/assets/icons/radix/line-height.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/link-1.svg b/assets/icons/radix/link-1.svg new file mode 100644 index 0000000000..d5682b113e --- /dev/null +++ b/assets/icons/radix/link-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/link-2.svg b/assets/icons/radix/link-2.svg new file mode 100644 index 0000000000..be8370606e --- /dev/null +++ b/assets/icons/radix/link-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/link-break-1.svg b/assets/icons/radix/link-break-1.svg new file mode 100644 index 0000000000..05ae93e47a --- /dev/null +++ b/assets/icons/radix/link-break-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/link-break-2.svg b/assets/icons/radix/link-break-2.svg new file mode 100644 index 0000000000..78f28f98e8 --- /dev/null +++ b/assets/icons/radix/link-break-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/link-none-1.svg b/assets/icons/radix/link-none-1.svg new file mode 100644 index 0000000000..6ea56a386f --- /dev/null +++ b/assets/icons/radix/link-none-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/link-none-2.svg b/assets/icons/radix/link-none-2.svg new file mode 100644 index 0000000000..0b19d940d1 --- /dev/null +++ b/assets/icons/radix/link-none-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/linkedin-logo.svg b/assets/icons/radix/linkedin-logo.svg new file mode 100644 index 0000000000..0f0138bdf6 --- /dev/null +++ b/assets/icons/radix/linkedin-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/list-bullet.svg b/assets/icons/radix/list-bullet.svg new file mode 100644 index 0000000000..2630b95ef0 --- /dev/null +++ b/assets/icons/radix/list-bullet.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/lock-closed.svg b/assets/icons/radix/lock-closed.svg new file mode 100644 index 0000000000..3871b5d5ad --- /dev/null +++ b/assets/icons/radix/lock-closed.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/lock-open-1.svg b/assets/icons/radix/lock-open-1.svg new file mode 100644 index 0000000000..8f6bfd5bbf --- /dev/null +++ b/assets/icons/radix/lock-open-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/lock-open-2.svg b/assets/icons/radix/lock-open-2.svg new file mode 100644 index 0000000000..ce69f67f29 --- /dev/null +++ b/assets/icons/radix/lock-open-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/loop.svg b/assets/icons/radix/loop.svg new file mode 100644 index 0000000000..bfa90ed084 --- /dev/null +++ b/assets/icons/radix/loop.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/magic-wand.svg b/assets/icons/radix/magic-wand.svg new file mode 100644 index 0000000000..bbc9826aa5 --- /dev/null +++ b/assets/icons/radix/magic-wand.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/magnifying-glass.svg b/assets/icons/radix/magnifying-glass.svg new file mode 100644 index 0000000000..a3a89bfa50 --- /dev/null +++ b/assets/icons/radix/magnifying-glass.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/margin.svg b/assets/icons/radix/margin.svg new file mode 100644 index 0000000000..1a513b37d6 --- /dev/null +++ b/assets/icons/radix/margin.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/mask-off.svg b/assets/icons/radix/mask-off.svg new file mode 100644 index 0000000000..5f847668e8 --- /dev/null +++ b/assets/icons/radix/mask-off.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/mask-on.svg b/assets/icons/radix/mask-on.svg new file mode 100644 index 0000000000..684c1b934d --- /dev/null +++ b/assets/icons/radix/mask-on.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/mic-mute.svg b/assets/icons/radix/mic-mute.svg new file mode 100644 index 0000000000..fe5f8201cc --- /dev/null +++ b/assets/icons/radix/mic-mute.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/radix/mic.svg b/assets/icons/radix/mic.svg new file mode 100644 index 0000000000..01f4c9bf66 --- /dev/null +++ b/assets/icons/radix/mic.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/radix/minus-circled.svg b/assets/icons/radix/minus-circled.svg new file mode 100644 index 0000000000..2c6df4cebf --- /dev/null +++ b/assets/icons/radix/minus-circled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/minus.svg b/assets/icons/radix/minus.svg new file mode 100644 index 0000000000..2b39602979 --- /dev/null +++ b/assets/icons/radix/minus.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/mix.svg b/assets/icons/radix/mix.svg new file mode 100644 index 0000000000..9412a01843 --- /dev/null +++ b/assets/icons/radix/mix.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/mixer-horizontal.svg b/assets/icons/radix/mixer-horizontal.svg new file mode 100644 index 0000000000..f29ba25548 --- /dev/null +++ b/assets/icons/radix/mixer-horizontal.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/mixer-vertical.svg b/assets/icons/radix/mixer-vertical.svg new file mode 100644 index 0000000000..dc85d3a9e7 --- /dev/null +++ b/assets/icons/radix/mixer-vertical.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/mobile.svg b/assets/icons/radix/mobile.svg new file mode 100644 index 0000000000..b62b6506ff --- /dev/null +++ b/assets/icons/radix/mobile.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/modulz-logo.svg b/assets/icons/radix/modulz-logo.svg new file mode 100644 index 0000000000..754b229db6 --- /dev/null +++ b/assets/icons/radix/modulz-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/moon.svg b/assets/icons/radix/moon.svg new file mode 100644 index 0000000000..1dac2ca212 --- /dev/null +++ b/assets/icons/radix/moon.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/move.svg b/assets/icons/radix/move.svg new file mode 100644 index 0000000000..3d0a0e56c9 --- /dev/null +++ b/assets/icons/radix/move.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/notion-logo.svg b/assets/icons/radix/notion-logo.svg new file mode 100644 index 0000000000..c2df152619 --- /dev/null +++ b/assets/icons/radix/notion-logo.svg @@ -0,0 +1,6 @@ + + + diff --git a/assets/icons/radix/opacity.svg b/assets/icons/radix/opacity.svg new file mode 100644 index 0000000000..a2d01bff82 --- /dev/null +++ b/assets/icons/radix/opacity.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/open-in-new-window.svg b/assets/icons/radix/open-in-new-window.svg new file mode 100644 index 0000000000..22baf82cff --- /dev/null +++ b/assets/icons/radix/open-in-new-window.svg @@ -0,0 +1,10 @@ + + + + + diff --git a/assets/icons/radix/outer-shadow.svg b/assets/icons/radix/outer-shadow.svg new file mode 100644 index 0000000000..b44e3d553c --- /dev/null +++ b/assets/icons/radix/outer-shadow.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/assets/icons/radix/overline.svg b/assets/icons/radix/overline.svg new file mode 100644 index 0000000000..57262c76e6 --- /dev/null +++ b/assets/icons/radix/overline.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/padding.svg b/assets/icons/radix/padding.svg new file mode 100644 index 0000000000..483a25a27e --- /dev/null +++ b/assets/icons/radix/padding.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/paper-plane.svg b/assets/icons/radix/paper-plane.svg new file mode 100644 index 0000000000..37ad070300 --- /dev/null +++ b/assets/icons/radix/paper-plane.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pause.svg b/assets/icons/radix/pause.svg new file mode 100644 index 0000000000..b399fb2f5a --- /dev/null +++ b/assets/icons/radix/pause.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pencil-1.svg b/assets/icons/radix/pencil-1.svg new file mode 100644 index 0000000000..decf0122ef --- /dev/null +++ b/assets/icons/radix/pencil-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pencil-2.svg b/assets/icons/radix/pencil-2.svg new file mode 100644 index 0000000000..2559a393a9 --- /dev/null +++ b/assets/icons/radix/pencil-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/person.svg b/assets/icons/radix/person.svg new file mode 100644 index 0000000000..051abcc703 --- /dev/null +++ b/assets/icons/radix/person.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pie-chart.svg b/assets/icons/radix/pie-chart.svg new file mode 100644 index 0000000000..bb58e47274 --- /dev/null +++ b/assets/icons/radix/pie-chart.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pilcrow.svg b/assets/icons/radix/pilcrow.svg new file mode 100644 index 0000000000..6996765fd6 --- /dev/null +++ b/assets/icons/radix/pilcrow.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pin-bottom.svg b/assets/icons/radix/pin-bottom.svg new file mode 100644 index 0000000000..ad0842054f --- /dev/null +++ b/assets/icons/radix/pin-bottom.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pin-left.svg b/assets/icons/radix/pin-left.svg new file mode 100644 index 0000000000..eb89b2912f --- /dev/null +++ b/assets/icons/radix/pin-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pin-right.svg b/assets/icons/radix/pin-right.svg new file mode 100644 index 0000000000..89a98bae4e --- /dev/null +++ b/assets/icons/radix/pin-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/pin-top.svg b/assets/icons/radix/pin-top.svg new file mode 100644 index 0000000000..edfeb64d5d --- /dev/null +++ b/assets/icons/radix/pin-top.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/play.svg b/assets/icons/radix/play.svg new file mode 100644 index 0000000000..92af9e1ae7 --- /dev/null +++ b/assets/icons/radix/play.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/plus-circled.svg b/assets/icons/radix/plus-circled.svg new file mode 100644 index 0000000000..808ddc4c2c --- /dev/null +++ b/assets/icons/radix/plus-circled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/plus.svg b/assets/icons/radix/plus.svg new file mode 100644 index 0000000000..57ce90219b --- /dev/null +++ b/assets/icons/radix/plus.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/question-mark-circled.svg b/assets/icons/radix/question-mark-circled.svg new file mode 100644 index 0000000000..be99968787 --- /dev/null +++ b/assets/icons/radix/question-mark-circled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/question-mark.svg b/assets/icons/radix/question-mark.svg new file mode 100644 index 0000000000..577aae5349 --- /dev/null +++ b/assets/icons/radix/question-mark.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/quote.svg b/assets/icons/radix/quote.svg new file mode 100644 index 0000000000..50205479c3 --- /dev/null +++ b/assets/icons/radix/quote.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/radiobutton.svg b/assets/icons/radix/radiobutton.svg new file mode 100644 index 0000000000..f0c3a60aee --- /dev/null +++ b/assets/icons/radix/radiobutton.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/reader.svg b/assets/icons/radix/reader.svg new file mode 100644 index 0000000000..e893cfa685 --- /dev/null +++ b/assets/icons/radix/reader.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/reload.svg b/assets/icons/radix/reload.svg new file mode 100644 index 0000000000..cf1dfb7fa2 --- /dev/null +++ b/assets/icons/radix/reload.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/reset.svg b/assets/icons/radix/reset.svg new file mode 100644 index 0000000000..f21a508514 --- /dev/null +++ b/assets/icons/radix/reset.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/resume.svg b/assets/icons/radix/resume.svg new file mode 100644 index 0000000000..79cdec2374 --- /dev/null +++ b/assets/icons/radix/resume.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/rocket.svg b/assets/icons/radix/rocket.svg new file mode 100644 index 0000000000..2226aacb1a --- /dev/null +++ b/assets/icons/radix/rocket.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/rotate-counter-clockwise.svg b/assets/icons/radix/rotate-counter-clockwise.svg new file mode 100644 index 0000000000..c43c90b90b --- /dev/null +++ b/assets/icons/radix/rotate-counter-clockwise.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/row-spacing.svg b/assets/icons/radix/row-spacing.svg new file mode 100644 index 0000000000..e155bd5947 --- /dev/null +++ b/assets/icons/radix/row-spacing.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/rows.svg b/assets/icons/radix/rows.svg new file mode 100644 index 0000000000..fb4ca0f9e3 --- /dev/null +++ b/assets/icons/radix/rows.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/ruler-horizontal.svg b/assets/icons/radix/ruler-horizontal.svg new file mode 100644 index 0000000000..db6f1ef488 --- /dev/null +++ b/assets/icons/radix/ruler-horizontal.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/ruler-square.svg b/assets/icons/radix/ruler-square.svg new file mode 100644 index 0000000000..7de70cc5dc --- /dev/null +++ b/assets/icons/radix/ruler-square.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/scissors.svg b/assets/icons/radix/scissors.svg new file mode 100644 index 0000000000..2893b34712 --- /dev/null +++ b/assets/icons/radix/scissors.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/section.svg b/assets/icons/radix/section.svg new file mode 100644 index 0000000000..1e939e2b2f --- /dev/null +++ b/assets/icons/radix/section.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/sewing-pin-filled.svg b/assets/icons/radix/sewing-pin-filled.svg new file mode 100644 index 0000000000..97f6f1120d --- /dev/null +++ b/assets/icons/radix/sewing-pin-filled.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/sewing-pin-solid.svg b/assets/icons/radix/sewing-pin-solid.svg new file mode 100644 index 0000000000..97f6f1120d --- /dev/null +++ b/assets/icons/radix/sewing-pin-solid.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/sewing-pin.svg b/assets/icons/radix/sewing-pin.svg new file mode 100644 index 0000000000..068dfd7bdf --- /dev/null +++ b/assets/icons/radix/sewing-pin.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/shadow-inner.svg b/assets/icons/radix/shadow-inner.svg new file mode 100644 index 0000000000..4d073bf35f --- /dev/null +++ b/assets/icons/radix/shadow-inner.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + diff --git a/assets/icons/radix/shadow-none.svg b/assets/icons/radix/shadow-none.svg new file mode 100644 index 0000000000..b02d3466ad --- /dev/null +++ b/assets/icons/radix/shadow-none.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + diff --git a/assets/icons/radix/shadow-outer.svg b/assets/icons/radix/shadow-outer.svg new file mode 100644 index 0000000000..dc7ea84087 --- /dev/null +++ b/assets/icons/radix/shadow-outer.svg @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/assets/icons/radix/shadow.svg b/assets/icons/radix/shadow.svg new file mode 100644 index 0000000000..c991af6156 --- /dev/null +++ b/assets/icons/radix/shadow.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + diff --git a/assets/icons/radix/share-1.svg b/assets/icons/radix/share-1.svg new file mode 100644 index 0000000000..58328e4d1e --- /dev/null +++ b/assets/icons/radix/share-1.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/share-2.svg b/assets/icons/radix/share-2.svg new file mode 100644 index 0000000000..1302ea5fbe --- /dev/null +++ b/assets/icons/radix/share-2.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/shuffle.svg b/assets/icons/radix/shuffle.svg new file mode 100644 index 0000000000..8670e1a048 --- /dev/null +++ b/assets/icons/radix/shuffle.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/size.svg b/assets/icons/radix/size.svg new file mode 100644 index 0000000000..dece8c5182 --- /dev/null +++ b/assets/icons/radix/size.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/sketch-logo.svg b/assets/icons/radix/sketch-logo.svg new file mode 100644 index 0000000000..6c54c4c825 --- /dev/null +++ b/assets/icons/radix/sketch-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/slash.svg b/assets/icons/radix/slash.svg new file mode 100644 index 0000000000..aa7dac30c1 --- /dev/null +++ b/assets/icons/radix/slash.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/slider.svg b/assets/icons/radix/slider.svg new file mode 100644 index 0000000000..66e0452bc0 --- /dev/null +++ b/assets/icons/radix/slider.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/space-between-horizontally.svg b/assets/icons/radix/space-between-horizontally.svg new file mode 100644 index 0000000000..a71638d52b --- /dev/null +++ b/assets/icons/radix/space-between-horizontally.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/space-between-vertically.svg b/assets/icons/radix/space-between-vertically.svg new file mode 100644 index 0000000000..bae247222f --- /dev/null +++ b/assets/icons/radix/space-between-vertically.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/space-evenly-horizontally.svg b/assets/icons/radix/space-evenly-horizontally.svg new file mode 100644 index 0000000000..70169492e4 --- /dev/null +++ b/assets/icons/radix/space-evenly-horizontally.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/space-evenly-vertically.svg b/assets/icons/radix/space-evenly-vertically.svg new file mode 100644 index 0000000000..469b4c05d4 --- /dev/null +++ b/assets/icons/radix/space-evenly-vertically.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/speaker-loud.svg b/assets/icons/radix/speaker-loud.svg new file mode 100644 index 0000000000..68982ee5e9 --- /dev/null +++ b/assets/icons/radix/speaker-loud.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/speaker-moderate.svg b/assets/icons/radix/speaker-moderate.svg new file mode 100644 index 0000000000..0f1d1b4210 --- /dev/null +++ b/assets/icons/radix/speaker-moderate.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/speaker-off.svg b/assets/icons/radix/speaker-off.svg new file mode 100644 index 0000000000..f60c35de7f --- /dev/null +++ b/assets/icons/radix/speaker-off.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/speaker-quiet.svg b/assets/icons/radix/speaker-quiet.svg new file mode 100644 index 0000000000..eb68cefcee --- /dev/null +++ b/assets/icons/radix/speaker-quiet.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/square.svg b/assets/icons/radix/square.svg new file mode 100644 index 0000000000..82843f51c3 --- /dev/null +++ b/assets/icons/radix/square.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/stack.svg b/assets/icons/radix/stack.svg new file mode 100644 index 0000000000..92426ffb0d --- /dev/null +++ b/assets/icons/radix/stack.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/star-filled.svg b/assets/icons/radix/star-filled.svg new file mode 100644 index 0000000000..2b17b7f579 --- /dev/null +++ b/assets/icons/radix/star-filled.svg @@ -0,0 +1,6 @@ + + + diff --git a/assets/icons/radix/star.svg b/assets/icons/radix/star.svg new file mode 100644 index 0000000000..23f09ad7b2 --- /dev/null +++ b/assets/icons/radix/star.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/stitches-logo.svg b/assets/icons/radix/stitches-logo.svg new file mode 100644 index 0000000000..319a1481f3 --- /dev/null +++ b/assets/icons/radix/stitches-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/stop.svg b/assets/icons/radix/stop.svg new file mode 100644 index 0000000000..57aac59cab --- /dev/null +++ b/assets/icons/radix/stop.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/stopwatch.svg b/assets/icons/radix/stopwatch.svg new file mode 100644 index 0000000000..ce5661e5cc --- /dev/null +++ b/assets/icons/radix/stopwatch.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/stretch-horizontally.svg b/assets/icons/radix/stretch-horizontally.svg new file mode 100644 index 0000000000..37977363b3 --- /dev/null +++ b/assets/icons/radix/stretch-horizontally.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/stretch-vertically.svg b/assets/icons/radix/stretch-vertically.svg new file mode 100644 index 0000000000..c4b1fe79ce --- /dev/null +++ b/assets/icons/radix/stretch-vertically.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/strikethrough.svg b/assets/icons/radix/strikethrough.svg new file mode 100644 index 0000000000..b814ef420a --- /dev/null +++ b/assets/icons/radix/strikethrough.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/sun.svg b/assets/icons/radix/sun.svg new file mode 100644 index 0000000000..1807a51b4c --- /dev/null +++ b/assets/icons/radix/sun.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/switch.svg b/assets/icons/radix/switch.svg new file mode 100644 index 0000000000..6dea528ce9 --- /dev/null +++ b/assets/icons/radix/switch.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/symbol.svg b/assets/icons/radix/symbol.svg new file mode 100644 index 0000000000..b529b2b08b --- /dev/null +++ b/assets/icons/radix/symbol.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/table.svg b/assets/icons/radix/table.svg new file mode 100644 index 0000000000..8ff059b847 --- /dev/null +++ b/assets/icons/radix/table.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/target.svg b/assets/icons/radix/target.svg new file mode 100644 index 0000000000..d67989e01f --- /dev/null +++ b/assets/icons/radix/target.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-align-bottom.svg b/assets/icons/radix/text-align-bottom.svg new file mode 100644 index 0000000000..862a5aeb88 --- /dev/null +++ b/assets/icons/radix/text-align-bottom.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-align-center.svg b/assets/icons/radix/text-align-center.svg new file mode 100644 index 0000000000..673cf8cd0a --- /dev/null +++ b/assets/icons/radix/text-align-center.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-align-justify.svg b/assets/icons/radix/text-align-justify.svg new file mode 100644 index 0000000000..df877f9513 --- /dev/null +++ b/assets/icons/radix/text-align-justify.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-align-left.svg b/assets/icons/radix/text-align-left.svg new file mode 100644 index 0000000000..b7a64fbd43 --- /dev/null +++ b/assets/icons/radix/text-align-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-align-middle.svg b/assets/icons/radix/text-align-middle.svg new file mode 100644 index 0000000000..e739d04efa --- /dev/null +++ b/assets/icons/radix/text-align-middle.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-align-right.svg b/assets/icons/radix/text-align-right.svg new file mode 100644 index 0000000000..e7609908ff --- /dev/null +++ b/assets/icons/radix/text-align-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-align-top.svg b/assets/icons/radix/text-align-top.svg new file mode 100644 index 0000000000..21660fe7d3 --- /dev/null +++ b/assets/icons/radix/text-align-top.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text-none.svg b/assets/icons/radix/text-none.svg new file mode 100644 index 0000000000..2a87f9372a --- /dev/null +++ b/assets/icons/radix/text-none.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/text.svg b/assets/icons/radix/text.svg new file mode 100644 index 0000000000..bd41d8ac19 --- /dev/null +++ b/assets/icons/radix/text.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/thick-arrow-down.svg b/assets/icons/radix/thick-arrow-down.svg new file mode 100644 index 0000000000..32923bec58 --- /dev/null +++ b/assets/icons/radix/thick-arrow-down.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/thick-arrow-left.svg b/assets/icons/radix/thick-arrow-left.svg new file mode 100644 index 0000000000..0cfd863903 --- /dev/null +++ b/assets/icons/radix/thick-arrow-left.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/thick-arrow-right.svg b/assets/icons/radix/thick-arrow-right.svg new file mode 100644 index 0000000000..a0cb605693 --- /dev/null +++ b/assets/icons/radix/thick-arrow-right.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/thick-arrow-up.svg b/assets/icons/radix/thick-arrow-up.svg new file mode 100644 index 0000000000..68687be28d --- /dev/null +++ b/assets/icons/radix/thick-arrow-up.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/timer.svg b/assets/icons/radix/timer.svg new file mode 100644 index 0000000000..20c52dff95 --- /dev/null +++ b/assets/icons/radix/timer.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/tokens.svg b/assets/icons/radix/tokens.svg new file mode 100644 index 0000000000..2bbbc82030 --- /dev/null +++ b/assets/icons/radix/tokens.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/track-next.svg b/assets/icons/radix/track-next.svg new file mode 100644 index 0000000000..24fd40e36f --- /dev/null +++ b/assets/icons/radix/track-next.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/track-previous.svg b/assets/icons/radix/track-previous.svg new file mode 100644 index 0000000000..d99e7ab53f --- /dev/null +++ b/assets/icons/radix/track-previous.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/transform.svg b/assets/icons/radix/transform.svg new file mode 100644 index 0000000000..e913ccc9a7 --- /dev/null +++ b/assets/icons/radix/transform.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/transparency-grid.svg b/assets/icons/radix/transparency-grid.svg new file mode 100644 index 0000000000..6559ef8c2b --- /dev/null +++ b/assets/icons/radix/transparency-grid.svg @@ -0,0 +1,9 @@ + + + diff --git a/assets/icons/radix/trash.svg b/assets/icons/radix/trash.svg new file mode 100644 index 0000000000..18780e492c --- /dev/null +++ b/assets/icons/radix/trash.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/triangle-down.svg b/assets/icons/radix/triangle-down.svg new file mode 100644 index 0000000000..ebfd8f2a12 --- /dev/null +++ b/assets/icons/radix/triangle-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/radix/triangle-left.svg b/assets/icons/radix/triangle-left.svg new file mode 100644 index 0000000000..0014139716 --- /dev/null +++ b/assets/icons/radix/triangle-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/radix/triangle-right.svg b/assets/icons/radix/triangle-right.svg new file mode 100644 index 0000000000..aed1393b9c --- /dev/null +++ b/assets/icons/radix/triangle-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/radix/triangle-up.svg b/assets/icons/radix/triangle-up.svg new file mode 100644 index 0000000000..5eb1b416d3 --- /dev/null +++ b/assets/icons/radix/triangle-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/radix/twitter-logo.svg b/assets/icons/radix/twitter-logo.svg new file mode 100644 index 0000000000..7dcf2f58eb --- /dev/null +++ b/assets/icons/radix/twitter-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/underline.svg b/assets/icons/radix/underline.svg new file mode 100644 index 0000000000..3344685097 --- /dev/null +++ b/assets/icons/radix/underline.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/update.svg b/assets/icons/radix/update.svg new file mode 100644 index 0000000000..b529b2b08b --- /dev/null +++ b/assets/icons/radix/update.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/upload.svg b/assets/icons/radix/upload.svg new file mode 100644 index 0000000000..a7f6bddb2e --- /dev/null +++ b/assets/icons/radix/upload.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/value-none.svg b/assets/icons/radix/value-none.svg new file mode 100644 index 0000000000..a86c08be1a --- /dev/null +++ b/assets/icons/radix/value-none.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/value.svg b/assets/icons/radix/value.svg new file mode 100644 index 0000000000..59dd7d9373 --- /dev/null +++ b/assets/icons/radix/value.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/vercel-logo.svg b/assets/icons/radix/vercel-logo.svg new file mode 100644 index 0000000000..5466fd9f0e --- /dev/null +++ b/assets/icons/radix/vercel-logo.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/video.svg b/assets/icons/radix/video.svg new file mode 100644 index 0000000000..e405396bef --- /dev/null +++ b/assets/icons/radix/video.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/view-grid.svg b/assets/icons/radix/view-grid.svg new file mode 100644 index 0000000000..04825a870b --- /dev/null +++ b/assets/icons/radix/view-grid.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/view-horizontal.svg b/assets/icons/radix/view-horizontal.svg new file mode 100644 index 0000000000..2ca7336b99 --- /dev/null +++ b/assets/icons/radix/view-horizontal.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/view-none.svg b/assets/icons/radix/view-none.svg new file mode 100644 index 0000000000..71b08a46d2 --- /dev/null +++ b/assets/icons/radix/view-none.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/view-vertical.svg b/assets/icons/radix/view-vertical.svg new file mode 100644 index 0000000000..0c8f8164b4 --- /dev/null +++ b/assets/icons/radix/view-vertical.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/width.svg b/assets/icons/radix/width.svg new file mode 100644 index 0000000000..3ae2b56e3d --- /dev/null +++ b/assets/icons/radix/width.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/zoom-in.svg b/assets/icons/radix/zoom-in.svg new file mode 100644 index 0000000000..caac722ad0 --- /dev/null +++ b/assets/icons/radix/zoom-in.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/radix/zoom-out.svg b/assets/icons/radix/zoom-out.svg new file mode 100644 index 0000000000..62046a9e0f --- /dev/null +++ b/assets/icons/radix/zoom-out.svg @@ -0,0 +1,8 @@ + + + diff --git a/assets/icons/split_message_15.svg b/assets/icons/split_message_15.svg new file mode 100644 index 0000000000..54d9e81224 --- /dev/null +++ b/assets/icons/split_message_15.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index 25143914cc..af845ae4f2 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -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" } }, { @@ -55,7 +53,40 @@ "context": "Pane", "bindings": { "alt-cmd-/": "search::ToggleRegex", - "ctrl-0": "project_panel::ToggleFocus" + "ctrl-0": "project_panel::ToggleFocus", + "cmd-1": [ + "pane::ActivateItem", + 0 + ], + "cmd-2": [ + "pane::ActivateItem", + 1 + ], + "cmd-3": [ + "pane::ActivateItem", + 2 + ], + "cmd-4": [ + "pane::ActivateItem", + 3 + ], + "cmd-5": [ + "pane::ActivateItem", + 4 + ], + "cmd-6": [ + "pane::ActivateItem", + 5 + ], + "cmd-7": [ + "pane::ActivateItem", + 6 + ], + "cmd-8": [ + "pane::ActivateItem", + 7 + ], + "cmd-9": "pane::ActivateLastItem" } }, { diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 45e85fd04f..6fc06198fe 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -40,7 +40,8 @@ "cmd-o": "workspace::Open", "alt-cmd-o": "projects::OpenRecent", "ctrl-~": "workspace::NewTerminal", - "ctrl-`": "terminal_panel::ToggleFocus" + "ctrl-`": "terminal_panel::ToggleFocus", + "shift-escape": "workspace::ToggleZoom" } }, { @@ -197,10 +198,20 @@ } }, { - "context": "AssistantEditor > Editor", + "context": "AssistantPanel", + "bindings": { + "cmd-g": "search::SelectNextMatch", + "cmd-shift-g": "search::SelectPrevMatch" + } + }, + { + "context": "ConversationEditor > Editor", "bindings": { "cmd-enter": "assistant::Assist", - "cmd->": "assistant::QuoteSelection" + "cmd-s": "workspace::Save", + "cmd->": "assistant::QuoteSelection", + "shift-enter": "assistant::Split", + "ctrl-r": "assistant::CycleMessageRole" } }, { @@ -232,8 +243,7 @@ "cmd-shift-g": "search::SelectPrevMatch", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ToggleRegex", - "shift-escape": "workspace::ToggleZoom" + "alt-cmd-r": "search::ToggleRegex" } }, // Bindings from VS Code @@ -398,6 +408,7 @@ "cmd-shift-p": "command_palette::Toggle", "cmd-shift-m": "diagnostics::Deploy", "cmd-shift-e": "project_panel::ToggleFocus", + "cmd-?": "assistant::ToggleFocus", "cmd-alt-s": "workspace::SaveAll", "cmd-k m": "language_selector::Toggle" } @@ -409,6 +420,7 @@ "ctrl-shift-k": "editor::DeleteLine", "cmd-shift-d": "editor::DuplicateLine", "cmd-shift-l": "editor::SplitSelectionIntoLines", + "ctrl-j": "editor::JoinLines", "ctrl-cmd-up": "editor::MoveLineUp", "ctrl-cmd-down": "editor::MoveLineDown", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", diff --git a/assets/keymaps/sublime_text.json b/assets/keymaps/sublime_text.json index 2d32b77d58..ca20802295 100644 --- a/assets/keymaps/sublime_text.json +++ b/assets/keymaps/sublime_text.json @@ -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" } }, { diff --git a/assets/keymaps/textmate.json b/assets/keymaps/textmate.json index 06be727429..591d6e443f 100644 --- a/assets/keymaps/textmate.json +++ b/assets/keymaps/textmate.json @@ -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", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 47c5f8c458..afee6fcd2e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1,6 +1,6 @@ [ { - "context": "Editor && VimControl && !VimWaiting", + "context": "Editor && VimControl && !VimWaiting && !menu", "bindings": { "g": [ "vim::PushOperator", @@ -25,11 +25,15 @@ } ], "h": "vim::Left", + "left": "vim::Left", "backspace": "vim::Backspace", "j": "vim::Down", + "down": "vim::Down", "enter": "vim::NextLineStart", "k": "vim::Up", + "up": "vim::Up", "l": "vim::Right", + "right": "vim::Right", "$": "vim::EndOfLine", "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", @@ -54,10 +58,6 @@ } ], "%": "vim::Matching", - "ctrl-y": [ - "vim::Scroll", - "LineUp" - ], "f": [ "vim::PushOperator", { @@ -90,6 +90,8 @@ } } ], + "ctrl-o": "pane::GoBack", + "ctrl-]": "editor::GoToDefinition", "escape": "editor::Cancel", "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ @@ -131,7 +133,7 @@ } }, { - "context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting", + "context": "Editor && vim_mode == normal && (vim_operator == none || vim_operator == n) && !VimWaiting", "bindings": { "c": [ "vim::PushOperator", @@ -143,6 +145,7 @@ "Delete" ], "shift-d": "vim::DeleteToEndOfLine", + "shift-j": "editor::JoinLines", "y": [ "vim::PushOperator", "Yank" @@ -165,6 +168,7 @@ "^": "vim::FirstNonWhitespace", "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", + "~": "vim::ChangeCase", "v": [ "vim::SwitchMode", { @@ -184,37 +188,29 @@ "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", - "ctrl-o": "pane::GoBack", "/": [ "buffer_search::Deploy", { "focus": true } ], - "ctrl-f": [ - "vim::Scroll", - "PageDown" - ], - "ctrl-b": [ - "vim::Scroll", - "PageUp" - ], - "ctrl-d": [ - "vim::Scroll", - "HalfPageDown" - ], - "ctrl-u": [ - "vim::Scroll", - "HalfPageUp" - ], - "ctrl-e": [ - "vim::Scroll", - "LineDown" - ], + "ctrl-f": "vim::PageDown", + "pagedown": "vim::PageDown", + "ctrl-b": "vim::PageUp", + "pageup": "vim::PageUp", + "ctrl-d": "vim::ScrollDown", + "ctrl-u": "vim::ScrollUp", + "ctrl-e": "vim::LineDown", + "ctrl-y": "vim::LineUp", "r": [ "vim::PushOperator", "Replace" - ] + ], + "s": "vim::Substitute", + "> >": "editor::Indent", + "< <": "editor::Outdent", + "ctrl-pagedown": "pane::ActivateNextItem", + "ctrl-pageup": "pane::ActivatePrevItem" } }, { @@ -231,6 +227,8 @@ "bindings": { "g": "vim::StartOfDocument", "h": "editor::Hover", + "t": "pane::ActivateNextItem", + "shift-t": "pane::ActivatePrevItem", "escape": [ "vim::SwitchMode", "Normal" @@ -301,10 +299,14 @@ "x": "vim::VisualDelete", "y": "vim::VisualYank", "p": "vim::VisualPaste", + "s": "vim::Substitute", + "~": "vim::ChangeCase", "r": [ "vim::PushOperator", "Replace" - ] + ], + "> >": "editor::Indent", + "< <": "editor::Outdent" } }, { diff --git a/assets/settings/default.json b/assets/settings/default.json index 8576c1ed65..9ae5c916b5 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -57,39 +57,49 @@ "show_whitespaces": "selection", // Scrollbar related settings "scrollbar": { - // When to show the scrollbar in the editor. - // This setting can take four values: - // - // 1. Show the scrollbar if there's important information or - // follow the system's configured behavior (default): - // "auto" - // 2. Match the system's configured behavior: - // "system" - // 3. Always show the scrollbar: - // "always" - // 4. Never show the scrollbar: - // "never" - "show": "auto", - // Whether to show git diff indicators in the scrollbar. - "git_diff": true, - // Whether to show selections in the scrollbar. - "selections": true + // When to show the scrollbar in the editor. + // This setting can take four values: + // + // 1. Show the scrollbar if there's important information or + // follow the system's configured behavior (default): + // "auto" + // 2. Match the system's configured behavior: + // "system" + // 3. Always show the scrollbar: + // "always" + // 4. Never show the scrollbar: + // "never" + "show": "auto", + // Whether to show git diff indicators in the scrollbar. + "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, + // Toggle certain types of hints on and off, all switched on by default. + "show_type_hints": true, + "show_parameter_hints": true, + // Corresponds to null/None LSP hint type value. + "show_other_hints": true }, "project_panel": { - // Whether to show the git status in the project panel. - "git_status": true, - // Where to dock project panel. Can be 'left' or 'right'. - "dock": "left", - // Default width of the project panel. - "default_width": 240 + // Whether to show the git status in the project panel. + "git_status": true, + // Where to dock project panel. Can be 'left' or 'right'. + "dock": "left", + // Default width of the project panel. + "default_width": 240 }, "assistant": { - // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. - "dock": "right", - // Default width when the assistant is docked to the left or right. - "default_width": 450, - // Default height when the assistant is docked to the bottom. - "default_height": 320 + // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. + "dock": "right", + // Default width when the assistant is docked to the left or right. + "default_width": 640, + // Default height when the assistant is docked to the bottom. + "default_height": 320 }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, diff --git a/assets/sounds/joined_call.wav b/assets/sounds/joined_call.wav new file mode 100644 index 0000000000..cf6e5ba4df Binary files /dev/null and b/assets/sounds/joined_call.wav differ diff --git a/assets/sounds/leave_call.wav b/assets/sounds/leave_call.wav new file mode 100644 index 0000000000..478b28204f Binary files /dev/null and b/assets/sounds/leave_call.wav differ diff --git a/assets/sounds/mute.wav b/assets/sounds/mute.wav new file mode 100644 index 0000000000..69e8456f6c Binary files /dev/null and b/assets/sounds/mute.wav differ diff --git a/assets/sounds/start_screenshare.wav b/assets/sounds/start_screenshare.wav new file mode 100644 index 0000000000..7b72a90af1 Binary files /dev/null and b/assets/sounds/start_screenshare.wav differ diff --git a/assets/sounds/stop_screenshare.wav b/assets/sounds/stop_screenshare.wav new file mode 100644 index 0000000000..1fe13e21b4 Binary files /dev/null and b/assets/sounds/stop_screenshare.wav differ diff --git a/assets/sounds/unmute.wav b/assets/sounds/unmute.wav new file mode 100644 index 0000000000..f8c90f6916 Binary files /dev/null and b/assets/sounds/unmute.wav differ diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 801c8f7172..8b46d7cfc5 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -207,16 +207,11 @@ impl ActivityIndicator { let mut checking_for_update = SmallVec::<[_; 3]>::new(); let mut failed = SmallVec::<[_; 3]>::new(); for status in &self.statuses { + let name = status.name.clone(); match status.status { - LanguageServerBinaryStatus::CheckingForUpdate => { - checking_for_update.push(status.name.clone()); - } - LanguageServerBinaryStatus::Downloading => { - downloading.push(status.name.clone()); - } - LanguageServerBinaryStatus::Failed { .. } => { - failed.push(status.name.clone()); - } + LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name), + LanguageServerBinaryStatus::Downloading => downloading.push(name), + LanguageServerBinaryStatus::Failed { .. } => failed.push(name), LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} } } @@ -326,7 +321,7 @@ impl View for ActivityIndicator { let mut element = MouseEventHandler::::new(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar.lsp_status; let style = if state.hovered() && on_click.is_some() { - theme.hover.as_ref().unwrap_or(&theme.default) + theme.hovered.as_ref().unwrap_or(&theme.default) } else { &theme.default }; diff --git a/crates/ai/Cargo.toml b/crates/ai/Cargo.toml index 9d67cbd108..013565e14f 100644 --- a/crates/ai/Cargo.toml +++ b/crates/ai/Cargo.toml @@ -22,13 +22,16 @@ util = { path = "../util" } workspace = { path = "../workspace" } anyhow.workspace = true -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } futures.workspace = true isahc.workspace = true +regex.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true +smol.workspace = true tiktoken-rs = "0.4" [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 40224b3229..812fb05121 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -1,19 +1,109 @@ pub mod assistant; mod assistant_settings; +use anyhow::Result; pub use assistant::AssistantPanel; +use chrono::{DateTime, Local}; +use collections::HashMap; +use fs::Fs; +use futures::StreamExt; use gpui::AppContext; +use regex::Regex; use serde::{Deserialize, Serialize}; -use std::fmt::{self, Display}; +use std::{ + cmp::Reverse, + fmt::{self, Display}, + path::PathBuf, + sync::Arc, +}; +use util::paths::CONVERSATIONS_DIR; // Data types for chat completion requests -#[derive(Serialize)] +#[derive(Debug, Serialize)] struct OpenAIRequest { model: String, messages: Vec, stream: bool, } +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +struct MessageId(usize); + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MessageMetadata { + role: Role, + sent_at: DateTime, + status: MessageStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum MessageStatus { + Pending, + Done, + Error(Arc), +} + +#[derive(Serialize, Deserialize)] +struct SavedMessage { + id: MessageId, + start: usize, +} + +#[derive(Serialize, Deserialize)] +struct SavedConversation { + zed: String, + version: String, + text: String, + messages: Vec, + message_metadata: HashMap, + summary: String, + model: String, +} + +impl SavedConversation { + const VERSION: &'static str = "0.1.0"; +} + +struct SavedConversationMetadata { + title: String, + path: PathBuf, + mtime: chrono::DateTime, +} + +impl SavedConversationMetadata { + pub async fn list(fs: Arc) -> Result> { + fs.create_dir(&CONVERSATIONS_DIR).await?; + + let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; + let mut conversations = Vec::::new(); + while let Some(path) = paths.next().await { + let path = path?; + + let pattern = r" - \d+.zed.json$"; + let re = Regex::new(pattern).unwrap(); + + let metadata = fs.metadata(&path).await?; + if let Some((file_name, metadata)) = path + .file_name() + .and_then(|name| name.to_str()) + .zip(metadata) + { + let title = re.replace(file_name, ""); + conversations.push(Self { + title: title.into_owned(), + path, + mtime: metadata.mtime.into(), + }); + } + } + conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); + + Ok(conversations) + } +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] struct RequestMessage { role: Role, diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index e5702cb677..4d300230e1 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,6 +1,7 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, - OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, + MessageId, MessageMetadata, MessageStatus, OpenAIRequest, OpenAIResponseStreamEvent, + RequestMessage, Role, SavedConversation, SavedConversationMetadata, SavedMessage, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; @@ -8,7 +9,7 @@ use collections::{HashMap, HashSet}; use editor::{ display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint}, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, ToOffset as _, + Anchor, Editor, ToOffset, }; use fs::Fs; use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt}; @@ -23,49 +24,66 @@ use gpui::{ }; use isahc::{http::StatusCode, Request, RequestExt}; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; +use search::BufferSearchBar; use serde::Deserialize; use settings::SettingsStore; use std::{ - borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc, + cell::RefCell, + cmp, env, + fmt::Write, + io, iter, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, time::Duration, }; -use util::{channel::ReleaseChannel, post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; +use theme::AssistantStyle; +use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, - item::Item, - pane, Pane, Workspace, + searchable::Direction, + Save, ToggleZoom, Toolbar, Workspace, }; const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; actions!( assistant, - [NewContext, Assist, QuoteSelection, ToggleFocus, ResetKey] + [ + NewConversation, + Assist, + Split, + CycleMessageRole, + QuoteSelection, + ToggleFocus, + ResetKey, + ] ); pub fn init(cx: &mut AppContext) { - if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable { - cx.update_default_global::(move |filter, _cx| { - filter.filtered_namespaces.insert("assistant"); - }); - } - settings::register::(cx); cx.add_action( - |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext| { - if let Some(this) = workspace.panel::(cx) { - this.update(cx, |this, cx| this.add_context(cx)) - } - - workspace.focus_panel::(cx); + |this: &mut AssistantPanel, + _: &workspace::NewFile, + cx: &mut ViewContext| { + this.new_conversation(cx); }, ); - cx.add_action(AssistantEditor::assist); - cx.capture_action(AssistantEditor::cancel_last_assist); - cx.add_action(AssistantEditor::quote_selection); - cx.capture_action(AssistantEditor::copy); + cx.add_action(ConversationEditor::assist); + cx.capture_action(ConversationEditor::cancel_last_assist); + cx.capture_action(ConversationEditor::save); + cx.add_action(ConversationEditor::quote_selection); + cx.capture_action(ConversationEditor::copy); + cx.add_action(ConversationEditor::split); + cx.capture_action(ConversationEditor::cycle_message_role); cx.add_action(AssistantPanel::save_api_key); cx.add_action(AssistantPanel::reset_api_key); + cx.add_action(AssistantPanel::toggle_zoom); + cx.add_action(AssistantPanel::deploy); + cx.add_action(AssistantPanel::select_next_match); + cx.add_action(AssistantPanel::select_prev_match); + cx.add_action(AssistantPanel::handle_editor_cancel); cx.add_action( |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { workspace.toggle_panel_focus::(cx); @@ -73,6 +91,7 @@ pub fn init(cx: &mut AppContext) { ); } +#[derive(Debug)] pub enum AssistantPanelEvent { ZoomIn, ZoomOut, @@ -82,15 +101,24 @@ pub enum AssistantPanelEvent { } pub struct AssistantPanel { + workspace: WeakViewHandle, width: Option, height: Option, - pane: ViewHandle, + active_editor_index: Option, + prev_active_editor_index: Option, + editors: Vec>, + saved_conversations: Vec, + saved_conversations_list_state: UniformListState, + zoomed: bool, + has_focus: bool, + toolbar: ViewHandle, api_key: Rc>>, api_key_editor: Option>, has_read_credentials: bool, languages: Arc, fs: Arc, subscriptions: Vec, + _watch_saved_conversations: Task>, } impl AssistantPanel { @@ -99,66 +127,52 @@ impl AssistantPanel { cx: AsyncAppContext, ) -> Task>> { cx.spawn(|mut cx| async move { + let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + let saved_conversations = SavedConversationMetadata::list(fs.clone()) + .await + .log_err() + .unwrap_or_default(); + // TODO: deserialize state. + let workspace_handle = workspace.clone(); workspace.update(&mut cx, |workspace, cx| { cx.add_view::(|cx| { - let weak_self = cx.weak_handle(); - let pane = cx.add_view(|cx| { - let mut pane = Pane::new( - workspace.weak_handle(), - workspace.project().clone(), - workspace.app_state().background_actions, - Default::default(), - cx, - ); - pane.set_can_split(false, cx); - pane.set_can_navigate(false, cx); - pane.on_can_drop(move |_, _| false); - pane.set_render_tab_bar_buttons(cx, move |pane, cx| { - let weak_self = weak_self.clone(); - Flex::row() - .with_child(Pane::render_tab_bar_button( - 0, - "icons/plus_12.svg", - false, - Some(("New Context".into(), Some(Box::new(NewContext)))), - cx, - move |_, cx| { - let weak_self = weak_self.clone(); - cx.window_context().defer(move |cx| { - if let Some(this) = weak_self.upgrade(cx) { - this.update(cx, |this, cx| this.add_context(cx)); - } - }) - }, - None, - )) - .with_child(Pane::render_tab_bar_button( - 1, - if pane.is_zoomed() { - "icons/minimize_8.svg" - } else { - "icons/maximize_8.svg" - }, - pane.is_zoomed(), - Some(( - "Toggle Zoom".into(), - Some(Box::new(workspace::ToggleZoom)), - )), - cx, - move |pane, cx| pane.toggle_zoom(&Default::default(), cx), - None, - )) - .into_any() - }); - let buffer_search_bar = cx.add_view(search::BufferSearchBar::new); - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx)); - pane + const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); + let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { + let mut events = fs + .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION) + .await; + while events.next().await.is_some() { + let saved_conversations = SavedConversationMetadata::list(fs.clone()) + .await + .log_err() + .unwrap_or_default(); + this.update(&mut cx, |this, cx| { + this.saved_conversations = saved_conversations; + cx.notify(); + }) + .ok(); + } + + anyhow::Ok(()) }); + let toolbar = cx.add_view(|cx| { + let mut toolbar = Toolbar::new(None); + toolbar.set_can_navigate(false, cx); + toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); + toolbar + }); let mut this = Self { - pane, + workspace: workspace_handle, + active_editor_index: Default::default(), + prev_active_editor_index: Default::default(), + editors: Default::default(), + saved_conversations, + saved_conversations_list_state: Default::default(), + zoomed: false, + has_focus: false, + toolbar, api_key: Rc::new(RefCell::new(None)), api_key_editor: None, has_read_credentials: false, @@ -167,20 +181,18 @@ impl AssistantPanel { width: None, height: None, subscriptions: Default::default(), + _watch_saved_conversations, }; let mut old_dock_position = this.position(cx); - this.subscriptions = vec![ - cx.observe(&this.pane, |_, _, cx| cx.notify()), - cx.subscribe(&this.pane, Self::handle_pane_event), - cx.observe_global::(move |this, cx| { + this.subscriptions = + vec![cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; cx.emit(AssistantPanelEvent::DockPositionChanged); } - }), - ]; + })]; this }) @@ -188,40 +200,64 @@ impl AssistantPanel { }) } - fn handle_pane_event( - &mut self, - _pane: ViewHandle, - event: &pane::Event, - cx: &mut ViewContext, - ) { - match event { - pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn), - pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut), - pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus), - pane::Event::Remove => cx.emit(AssistantPanelEvent::Close), - _ => {} - } - } - - fn add_context(&mut self, cx: &mut ViewContext) { - let focus = self.has_focus(cx); - let editor = cx - .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx)); - self.subscriptions - .push(cx.subscribe(&editor, Self::handle_assistant_editor_event)); - self.pane.update(cx, |pane, cx| { - pane.add_item(Box::new(editor), true, focus, None, cx) + fn new_conversation(&mut self, cx: &mut ViewContext) -> ViewHandle { + let editor = cx.add_view(|cx| { + ConversationEditor::new( + self.api_key.clone(), + self.languages.clone(), + self.fs.clone(), + cx, + ) }); + self.add_conversation(editor.clone(), cx); + editor } - fn handle_assistant_editor_event( + fn add_conversation( &mut self, - _: ViewHandle, - event: &AssistantEditorEvent, + editor: ViewHandle, + cx: &mut ViewContext, + ) { + self.subscriptions + .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); + + let conversation = editor.read(cx).conversation.clone(); + self.subscriptions + .push(cx.observe(&conversation, |_, _, cx| cx.notify())); + + let index = self.editors.len(); + self.editors.push(editor); + self.set_active_editor_index(Some(index), cx); + } + + fn set_active_editor_index(&mut self, index: Option, cx: &mut ViewContext) { + self.prev_active_editor_index = self.active_editor_index; + self.active_editor_index = index; + if let Some(editor) = self.active_editor() { + let editor = editor.read(cx).editor.clone(); + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(Some(&editor), cx); + }); + if self.has_focus(cx) { + cx.focus(&editor); + } + } else { + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(None, cx); + }); + } + + cx.notify(); + } + + fn handle_conversation_editor_event( + &mut self, + _: ViewHandle, + event: &ConversationEditorEvent, cx: &mut ViewContext, ) { match event { - AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()), + ConversationEditorEvent::TabContentChanged => cx.notify(), } } @@ -252,6 +288,287 @@ impl AssistantPanel { cx.focus_self(); cx.notify(); } + + fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { + if self.zoomed { + cx.emit(AssistantPanelEvent::ZoomOut) + } else { + cx.emit(AssistantPanelEvent::ZoomIn) + } + } + + fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) { + return; + } + } + cx.propagate_action(); + } + + fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + if !search_bar.read(cx).is_dismissed() { + search_bar.update(cx, |search_bar, cx| { + search_bar.dismiss(&Default::default(), cx) + }); + return; + } + } + cx.propagate_action(); + } + + fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx)); + } + } + + fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx)); + } + } + + fn active_editor(&self) -> Option<&ViewHandle> { + self.editors.get(self.active_editor_index?) + } + + fn render_hamburger_button(cx: &mut ViewContext) -> impl Element { + enum History {} + let theme = theme::current(cx); + let tooltip_style = theme::current(cx).tooltip.clone(); + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.assistant.hamburger_button.style_for(state); + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this: &mut Self, cx| { + if this.active_editor().is_some() { + this.set_active_editor_index(None, cx); + } else { + this.set_active_editor_index(this.prev_active_editor_index, cx); + } + }) + .with_tooltip::(1, "History".into(), None, tooltip_style, cx) + } + + fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec> { + if self.active_editor().is_some() { + vec![ + Self::render_split_button(cx).into_any(), + Self::render_quote_button(cx).into_any(), + Self::render_assist_button(cx).into_any(), + ] + } else { + Default::default() + } + } + + fn render_split_button(cx: &mut ViewContext) -> impl Element { + let theme = theme::current(cx); + let tooltip_style = theme::current(cx).tooltip.clone(); + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.assistant.split_button.style_for(state); + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this: &mut Self, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); + } + }) + .with_tooltip::( + 1, + "Split Message".into(), + Some(Box::new(Split)), + tooltip_style, + cx, + ) + } + + fn render_assist_button(cx: &mut ViewContext) -> impl Element { + let theme = theme::current(cx); + let tooltip_style = theme::current(cx).tooltip.clone(); + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.assistant.assist_button.style_for(state); + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this: &mut Self, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); + } + }) + .with_tooltip::( + 1, + "Assist".into(), + Some(Box::new(Assist)), + tooltip_style, + cx, + ) + } + + fn render_quote_button(cx: &mut ViewContext) -> impl Element { + let theme = theme::current(cx); + let tooltip_style = theme::current(cx).tooltip.clone(); + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.assistant.quote_button.style_for(state); + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this: &mut Self, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + ConversationEditor::quote_selection(workspace, &Default::default(), cx) + }); + }); + } + }) + .with_tooltip::( + 1, + "Quote Selection".into(), + Some(Box::new(QuoteSelection)), + tooltip_style, + cx, + ) + } + + fn render_plus_button(cx: &mut ViewContext) -> impl Element { + let theme = theme::current(cx); + let tooltip_style = theme::current(cx).tooltip.clone(); + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.assistant.plus_button.style_for(state); + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this: &mut Self, cx| { + this.new_conversation(cx); + }) + .with_tooltip::( + 1, + "New Conversation".into(), + Some(Box::new(NewConversation)), + tooltip_style, + cx, + ) + } + + fn render_zoom_button(&self, cx: &mut ViewContext) -> impl Element { + enum ToggleZoomButton {} + + let theme = theme::current(cx); + let tooltip_style = theme::current(cx).tooltip.clone(); + let style = if self.zoomed { + &theme.assistant.zoom_out_button + } else { + &theme.assistant.zoom_in_button + }; + + MouseEventHandler::::new(0, cx, |state, _| { + let style = style.style_for(state); + Svg::for_style(style.icon.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| { + this.toggle_zoom(&ToggleZoom, cx); + }) + .with_tooltip::( + 0, + if self.zoomed { + "Zoom Out".into() + } else { + "Zoom In".into() + }, + Some(Box::new(ToggleZoom)), + tooltip_style, + cx, + ) + } + + fn render_saved_conversation( + &mut self, + index: usize, + cx: &mut ViewContext, + ) -> impl Element { + let conversation = &self.saved_conversations[index]; + let path = conversation.path.clone(); + MouseEventHandler::::new(index, cx, move |state, cx| { + let style = &theme::current(cx).assistant.saved_conversation; + Flex::row() + .with_child( + Label::new( + conversation.mtime.format("%F %I:%M%p").to_string(), + style.saved_at.text.clone(), + ) + .aligned() + .contained() + .with_style(style.saved_at.container), + ) + .with_child( + Label::new(conversation.title.clone(), style.title.text.clone()) + .aligned() + .contained() + .with_style(style.title.container), + ) + .contained() + .with_style(*style.container.style_for(state)) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.open_conversation(path.clone(), cx) + .detach_and_log_err(cx) + }) + } + + fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { + if let Some(ix) = self.editor_index_for_path(&path, cx) { + self.set_active_editor_index(Some(ix), cx); + return Task::ready(Ok(())); + } + + let fs = self.fs.clone(); + let api_key = self.api_key.clone(); + let languages = self.languages.clone(); + cx.spawn(|this, mut cx| async move { + let saved_conversation = fs.load(&path).await?; + let saved_conversation = serde_json::from_str(&saved_conversation)?; + let conversation = cx.add_model(|cx| { + Conversation::deserialize(saved_conversation, path.clone(), api_key, languages, cx) + }); + this.update(&mut cx, |this, cx| { + // If, by the time we've loaded the conversation, the user has already opened + // the same conversation, we don't want to open it again. + if let Some(ix) = this.editor_index_for_path(&path, cx) { + this.set_active_editor_index(Some(ix), cx); + } else { + let editor = cx + .add_view(|cx| ConversationEditor::for_conversation(conversation, fs, cx)); + this.add_conversation(editor, cx); + } + })?; + Ok(()) + }) + } + + fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option { + self.editors + .iter() + .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) + } } fn build_api_key_editor(cx: &mut ViewContext) -> ViewHandle { @@ -275,7 +592,8 @@ impl View for AssistantPanel { } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let style = &theme::current(cx).assistant; + let theme = &theme::current(cx); + let style = &theme.assistant; if let Some(api_key_editor) = self.api_key_editor.as_ref() { Flex::column() .with_child( @@ -296,19 +614,81 @@ impl View for AssistantPanel { .aligned() .into_any() } else { - ChildView::new(&self.pane, cx).into_any() + let title = self.active_editor().map(|editor| { + Label::new(editor.read(cx).title(cx), style.title.text.clone()) + .contained() + .with_style(style.title.container) + .aligned() + .left() + .flex(1., false) + }); + let mut header = Flex::row() + .with_child(Self::render_hamburger_button(cx).aligned()) + .with_children(title); + if self.has_focus { + header.add_children( + self.render_editor_tools(cx) + .into_iter() + .map(|tool| tool.aligned().flex_float()), + ); + header.add_child(Self::render_plus_button(cx).aligned().flex_float()); + header.add_child(self.render_zoom_button(cx).aligned()); + } + + Flex::column() + .with_child( + header + .contained() + .with_style(theme.workspace.tab_bar.container) + .expanded() + .constrained() + .with_height(theme.workspace.tab_bar.height), + ) + .with_children(if self.toolbar.read(cx).hidden() { + None + } else { + Some(ChildView::new(&self.toolbar, cx).expanded()) + }) + .with_child(if let Some(editor) = self.active_editor() { + ChildView::new(editor, cx).flex(1., true).into_any() + } else { + UniformList::new( + self.saved_conversations_list_state.clone(), + self.saved_conversations.len(), + cx, + |this, range, items, cx| { + for ix in range { + items.push(this.render_saved_conversation(ix, cx).into_any()); + } + }, + ) + .flex(1., true) + .into_any() + }) + .into_any() } } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); + cx.notify(); if cx.is_self_focused() { - if let Some(api_key_editor) = self.api_key_editor.as_ref() { + if let Some(editor) = self.active_editor() { + cx.focus(editor); + } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { cx.focus(api_key_editor); - } else { - cx.focus(&self.pane); } } } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = false; + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); + cx.notify(); + } } impl Panel for AssistantPanel { @@ -361,19 +741,22 @@ impl Panel for AssistantPanel { matches!(event, AssistantPanelEvent::ZoomOut) } - fn is_zoomed(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).is_zoomed() + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed } fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { - self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + self.zoomed = zoomed; + cx.notify(); } fn set_active(&mut self, active: bool, cx: &mut ViewContext) { if active { if self.api_key.borrow().is_none() && !self.has_read_credentials { self.has_read_credentials = true; - let api_key = if let Some((_, api_key)) = cx + let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") { + Some(api_key) + } else if let Some((_, api_key)) = cx .platform() .read_credentials(OPENAI_API_URL) .log_err() @@ -391,8 +774,8 @@ impl Panel for AssistantPanel { } } - if self.pane.read(cx).items_len() == 0 { - self.add_context(cx); + if self.editors.is_empty() { + self.new_conversation(cx); } } } @@ -417,12 +800,8 @@ impl Panel for AssistantPanel { matches!(event, AssistantPanelEvent::Close) } - fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus() - || self - .api_key_editor - .as_ref() - .map_or(false, |editor| editor.is_focused(cx)) + fn has_focus(&self, _: &WindowContext) -> bool { + self.has_focus } fn is_focus_event(event: &Self::Event) -> bool { @@ -430,18 +809,24 @@ impl Panel for AssistantPanel { } } -enum AssistantEvent { +enum ConversationEvent { MessagesEdited, SummaryChanged, StreamedCompletion, } -struct Assistant { +#[derive(Default)] +struct Summary { + text: String, + done: bool, +} + +struct Conversation { buffer: ModelHandle, - messages: Vec, + message_anchors: Vec, messages_metadata: HashMap, next_message_id: MessageId, - summary: Option, + summary: Option, pending_summary: Task>, completion_count: usize, pending_completions: Vec, @@ -450,20 +835,22 @@ struct Assistant { max_token_count: usize, pending_token_count: Task>, api_key: Rc>>, + pending_save: Task>, + path: Option, _subscriptions: Vec, } -impl Entity for Assistant { - type Event = AssistantEvent; +impl Entity for Conversation { + type Event = ConversationEvent; } -impl Assistant { +impl Conversation { fn new( api_key: Rc>>, language_registry: Arc, cx: &mut ModelContext, ) -> Self { - let model = "gpt-3.5-turbo"; + let model = "gpt-3.5-turbo-0613"; let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { let mut buffer = Buffer::new(0, "", cx); @@ -483,7 +870,7 @@ impl Assistant { }); let mut this = Self { - messages: Default::default(), + message_anchors: Default::default(), messages_metadata: Default::default(), next_message_id: Default::default(), summary: None, @@ -495,20 +882,22 @@ impl Assistant { pending_token_count: Task::ready(None), model: model.into(), _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], + pending_save: Task::ready(Ok(())), + path: None, api_key, buffer, }; - let message = Message { + let message = MessageAnchor { id: MessageId(post_inc(&mut this.next_message_id.0)), start: language::Anchor::MIN, }; - this.messages.push(message.clone()); + this.message_anchors.push(message.clone()); this.messages_metadata.insert( message.id, MessageMetadata { role: Role::User, sent_at: Local::now(), - error: None, + status: MessageStatus::Done, }, ); @@ -516,6 +905,88 @@ impl Assistant { this } + fn serialize(&self, cx: &AppContext) -> SavedConversation { + SavedConversation { + zed: "conversation".into(), + version: SavedConversation::VERSION.into(), + text: self.buffer.read(cx).text(), + message_metadata: self.messages_metadata.clone(), + messages: self + .messages(cx) + .map(|message| SavedMessage { + id: message.id, + start: message.offset_range.start, + }) + .collect(), + summary: self + .summary + .as_ref() + .map(|summary| summary.text.clone()) + .unwrap_or_default(), + model: self.model.clone(), + } + } + + fn deserialize( + saved_conversation: SavedConversation, + path: PathBuf, + api_key: Rc>>, + language_registry: Arc, + cx: &mut ModelContext, + ) -> Self { + let model = saved_conversation.model; + let markdown = language_registry.language_for_name("Markdown"); + let mut message_anchors = Vec::new(); + let mut next_message_id = MessageId(0); + let buffer = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, saved_conversation.text, cx); + for message in saved_conversation.messages { + message_anchors.push(MessageAnchor { + id: message.id, + start: buffer.anchor_before(message.start), + }); + next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); + } + buffer.set_language_registry(language_registry); + cx.spawn_weak(|buffer, mut cx| async move { + let markdown = markdown.await?; + let buffer = buffer + .upgrade(&cx) + .ok_or_else(|| anyhow!("buffer was dropped"))?; + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx) + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + buffer + }); + + let mut this = Self { + message_anchors, + messages_metadata: saved_conversation.message_metadata, + next_message_id, + summary: Some(Summary { + text: saved_conversation.summary, + done: true, + }), + pending_summary: Task::ready(None), + completion_count: Default::default(), + pending_completions: Default::default(), + token_count: None, + max_token_count: tiktoken_rs::model::get_context_size(&model), + pending_token_count: Task::ready(None), + model, + _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], + pending_save: Task::ready(Ok(())), + path: Some(path), + api_key, + buffer, + }; + this.count_remaining_tokens(cx); + this + } + fn handle_buffer_event( &mut self, _: ModelHandle, @@ -525,7 +996,7 @@ impl Assistant { match event { language::Event::Edited => { self.count_remaining_tokens(cx); - cx.emit(AssistantEvent::MessagesEdited); + cx.emit(ConversationEvent::MessagesEdited); } _ => {} } @@ -533,7 +1004,7 @@ impl Assistant { fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { let messages = self - .open_ai_request_messages(cx) + .messages(cx) .into_iter() .filter_map(|message| { Some(tiktoken_rs::ChatCompletionRequestMessage { @@ -542,7 +1013,11 @@ impl Assistant { Role::Assistant => "assistant".into(), Role::System => "system".into(), }, - content: message.content, + content: self + .buffer + .read(cx) + .text_for_range(message.offset_range) + .collect(), name: None, }) }) @@ -557,7 +1032,7 @@ impl Assistant { .await?; this.upgrade(&cx) - .ok_or_else(|| anyhow!("assistant was dropped"))? + .ok_or_else(|| anyhow!("conversation was dropped"))? .update(&mut cx, |this, cx| { this.max_token_count = tiktoken_rs::model::get_context_size(&this.model); this.token_count = Some(token_count); @@ -579,96 +1054,190 @@ impl Assistant { cx.notify(); } - fn assist(&mut self, cx: &mut ModelContext) -> Option<(Message, Message)> { - let request = OpenAIRequest { - model: self.model.clone(), - messages: self.open_ai_request_messages(cx), - stream: true, - }; + fn assist( + &mut self, + selected_messages: HashSet, + cx: &mut ModelContext, + ) -> Vec { + let mut user_messages = Vec::new(); + let mut tasks = Vec::new(); - let api_key = self.api_key.borrow().clone()?; - let stream = stream_completion(api_key, cx.background().clone(), request); - let assistant_message = - self.insert_message_after(self.messages.last()?.id, Role::Assistant, cx)?; - let user_message = self.insert_message_after(assistant_message.id, Role::User, cx)?; - let task = cx.spawn_weak({ - |this, mut cx| async move { - let assistant_message_id = assistant_message.id; - let stream_completion = async { - let mut messages = stream.await?; + let last_message_id = self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + }); - while let Some(message) = messages.next().await { - let mut message = message?; - if let Some(choice) = message.choices.pop() { - this.upgrade(&cx) - .ok_or_else(|| anyhow!("assistant was dropped"))? - .update(&mut cx, |this, cx| { - let text: Arc = choice.delta.content?.into(); - let message_ix = this - .messages - .iter() - .position(|message| message.id == assistant_message_id)?; - this.buffer.update(cx, |buffer, cx| { - let offset = if message_ix + 1 == this.messages.len() { - buffer.len() - } else { - this.messages[message_ix + 1] - .start - .to_offset(buffer) - .saturating_sub(1) - }; - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(AssistantEvent::StreamedCompletion); - - Some(()) - }); - } - } - - this.upgrade(&cx) - .ok_or_else(|| anyhow!("assistant was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - }); - - anyhow::Ok(()) + for selected_message_id in selected_messages { + let selected_message_role = + if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { + metadata.role + } else { + continue; }; - let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Err(error) = result { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - metadata.error = Some(error.to_string().trim().into()); - cx.notify(); - } - } - }); + if selected_message_role == Role::Assistant { + if let Some(user_message) = self.insert_message_after( + selected_message_id, + Role::User, + MessageStatus::Done, + cx, + ) { + user_messages.push(user_message); + } else { + continue; } - } - }); + } else { + let request = OpenAIRequest { + model: self.model.clone(), + messages: self + .messages(cx) + .filter(|message| matches!(message.status, MessageStatus::Done)) + .flat_map(|message| { + let mut system_message = None; + if message.id == selected_message_id { + system_message = Some(RequestMessage { + role: Role::System, + content: concat!( + "Treat the following messages as additional knowledge you have learned about, ", + "but act as if they were not part of this conversation. That is, treat them ", + "as if the user didn't see them and couldn't possibly inquire about them." + ).into() + }); + } - self.pending_completions.push(PendingCompletion { - id: post_inc(&mut self.completion_count), - _task: task, - }); - Some((assistant_message, user_message)) + Some(message.to_open_ai_message(self.buffer.read(cx))).into_iter().chain(system_message) + }) + .chain(Some(RequestMessage { + role: Role::System, + content: format!( + "Direct your reply to message with id {}. Do not include a [Message X] header.", + selected_message_id.0 + ), + })) + .collect(), + stream: true, + }; + + let Some(api_key) = self.api_key.borrow().clone() else { continue }; + let stream = stream_completion(api_key, cx.background().clone(), request); + let assistant_message = self + .insert_message_after( + selected_message_id, + Role::Assistant, + MessageStatus::Pending, + cx, + ) + .unwrap(); + + // Queue up the user's next reply + if Some(selected_message_id) == last_message_id { + let user_message = self + .insert_message_after( + assistant_message.id, + Role::User, + MessageStatus::Done, + cx, + ) + .unwrap(); + user_messages.push(user_message); + } + + tasks.push(cx.spawn_weak({ + |this, mut cx| async move { + let assistant_message_id = assistant_message.id; + let stream_completion = async { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let mut message = message?; + if let Some(choice) = message.choices.pop() { + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + let text: Arc = choice.delta.content?.into(); + let message_ix = this.message_anchors.iter().position( + |message| message.id == assistant_message_id, + )?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] + .iter() + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message + .start + .to_offset(buffer) + .saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); + }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + }); + } + smol::future::yield_now().await; + } + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions.retain(|completion| { + completion.id != this.completion_count + }); + this.summarize(cx); + }); + + anyhow::Ok(()) + }; + + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = MessageStatus::Error( + error.to_string().trim().into(), + ); + } + } + cx.notify(); + } + }); + } + } + })); + } + } + + if !tasks.is_empty() { + self.pending_completions.push(PendingCompletion { + id: post_inc(&mut self.completion_count), + _tasks: tasks, + }); + } + + user_messages } fn cancel_last_assist(&mut self) -> bool { self.pending_completions.pop().is_some() } - fn cycle_message_role(&mut self, id: MessageId, cx: &mut ModelContext) { - if let Some(metadata) = self.messages_metadata.get_mut(&id) { - metadata.role.cycle(); - cx.emit(AssistantEvent::MessagesEdited); - cx.notify(); + fn cycle_message_roles(&mut self, ids: HashSet, cx: &mut ModelContext) { + for id in ids { + if let Some(metadata) = self.messages_metadata.get_mut(&id) { + metadata.role.cycle(); + cx.emit(ConversationEvent::MessagesEdited); + cx.notify(); + } } } @@ -676,55 +1245,179 @@ impl Assistant { &mut self, message_id: MessageId, role: Role, + status: MessageStatus, cx: &mut ModelContext, - ) -> Option { + ) -> Option { if let Some(prev_message_ix) = self - .messages + .message_anchors .iter() .position(|message| message.id == message_id) { + // Find the next valid message after the one we were given. + let mut next_message_ix = prev_message_ix + 1; + while let Some(next_message) = self.message_anchors.get(next_message_ix) { + if next_message.start.is_valid(self.buffer.read(cx)) { + break; + } + next_message_ix += 1; + } + let start = self.buffer.update(cx, |buffer, cx| { - let offset = self.messages[prev_message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) + let offset = self + .message_anchors + .get(next_message_ix) .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1); buffer.edit([(offset..offset, "\n")], None, cx); buffer.anchor_before(offset + 1) }); - let message = Message { + let message = MessageAnchor { id: MessageId(post_inc(&mut self.next_message_id.0)), start, }; - self.messages.insert(prev_message_ix + 1, message.clone()); + self.message_anchors + .insert(next_message_ix, message.clone()); self.messages_metadata.insert( message.id, MessageMetadata { role, sent_at: Local::now(), - error: None, + status, }, ); - cx.emit(AssistantEvent::MessagesEdited); + cx.emit(ConversationEvent::MessagesEdited); Some(message) } else { None } } + fn split_message( + &mut self, + range: Range, + cx: &mut ModelContext, + ) -> (Option, Option) { + let start_message = self.message_for_offset(range.start, cx); + let end_message = self.message_for_offset(range.end, cx); + if let Some((start_message, end_message)) = start_message.zip(end_message) { + // Prevent splitting when range spans multiple messages. + if start_message.id != end_message.id { + return (None, None); + } + + let message = start_message; + let role = message.role; + let mut edited_buffer = false; + + let mut suffix_start = None; + if range.start > message.offset_range.start && range.end < message.offset_range.end - 1 + { + if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') { + suffix_start = Some(range.end + 1); + } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') { + suffix_start = Some(range.end); + } + } + + let suffix = if let Some(suffix_start) = suffix_start { + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(suffix_start), + } + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit([(range.end..range.end, "\n")], None, cx); + }); + edited_buffer = true; + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(range.end + 1), + } + }; + + self.message_anchors + .insert(message.index_range.end + 1, suffix.clone()); + self.messages_metadata.insert( + suffix.id, + MessageMetadata { + role, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + + let new_messages = + if range.start == range.end || range.start == message.offset_range.start { + (None, Some(suffix)) + } else { + let mut prefix_end = None; + if range.start > message.offset_range.start + && range.end < message.offset_range.end - 1 + { + if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') { + prefix_end = Some(range.start + 1); + } else if self.buffer.read(cx).reversed_chars_at(range.start).next() + == Some('\n') + { + prefix_end = Some(range.start); + } + } + + let selection = if let Some(prefix_end) = prefix_end { + cx.emit(ConversationEvent::MessagesEdited); + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(prefix_end), + } + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit([(range.start..range.start, "\n")], None, cx) + }); + edited_buffer = true; + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(range.end + 1), + } + }; + + self.message_anchors + .insert(message.index_range.end + 1, selection.clone()); + self.messages_metadata.insert( + selection.id, + MessageMetadata { + role, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + (Some(selection), Some(suffix)) + }; + + if !edited_buffer { + cx.emit(ConversationEvent::MessagesEdited); + } + new_messages + } else { + (None, None) + } + } + fn summarize(&mut self, cx: &mut ModelContext) { - if self.messages.len() >= 2 && self.summary.is_none() { + if self.message_anchors.len() >= 2 && self.summary.is_none() { let api_key = self.api_key.borrow().clone(); if let Some(api_key) = api_key { - let mut messages = self.open_ai_request_messages(cx); - messages.truncate(2); - messages.push(RequestMessage { - role: Role::User, - content: "Summarize the conversation into a short title without punctuation" - .into(), - }); + let messages = self + .messages(cx) + .take(2) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .chain(Some(RequestMessage { + role: Role::User, + content: + "Summarize the conversation into a short title without punctuation" + .into(), + })); let request = OpenAIRequest { model: self.model.clone(), - messages, + messages: messages.collect(), stream: true, }; @@ -738,12 +1431,22 @@ impl Assistant { if let Some(choice) = message.choices.pop() { let text = choice.delta.content.unwrap_or_default(); this.update(&mut cx, |this, cx| { - this.summary.get_or_insert(String::new()).push_str(&text); - cx.emit(AssistantEvent::SummaryChanged); + this.summary + .get_or_insert(Default::default()) + .text + .push_str(&text); + cx.emit(ConversationEvent::SummaryChanged); }); } } + this.update(&mut cx, |this, cx| { + if let Some(summary) = this.summary.as_mut() { + summary.done = true; + cx.emit(ConversationEvent::SummaryChanged); + } + }); + anyhow::Ok(()) } .log_err() @@ -752,61 +1455,140 @@ impl Assistant { } } - fn open_ai_request_messages(&self, cx: &AppContext) -> Vec { - let buffer = self.buffer.read(cx); - self.messages(cx) - .map(|(_message, metadata, range)| RequestMessage { - role: metadata.role, - content: buffer.text_for_range(range).collect(), - }) - .collect() + fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option { + self.messages_for_offsets([offset], cx).pop() } - fn message_id_for_offset(&self, offset: usize, cx: &AppContext) -> Option { - Some( - self.messages(cx) - .find(|(_, _, range)| range.contains(&offset)) - .map(|(message, _, _)| message) - .or(self.messages.last())? - .id, - ) + fn messages_for_offsets( + &self, + offsets: impl IntoIterator, + cx: &AppContext, + ) -> Vec { + let mut result = Vec::new(); + + let mut messages = self.messages(cx).peekable(); + let mut offsets = offsets.into_iter().peekable(); + let mut current_message = messages.next(); + while let Some(offset) = offsets.next() { + // Locate the message that contains the offset. + while current_message.as_ref().map_or(false, |message| { + !message.offset_range.contains(&offset) && messages.peek().is_some() + }) { + current_message = messages.next(); + } + let Some(message) = current_message.as_ref() else { break }; + + // Skip offsets that are in the same message. + while offsets.peek().map_or(false, |offset| { + message.offset_range.contains(offset) || messages.peek().is_none() + }) { + offsets.next(); + } + + result.push(message.clone()); + } + result } - fn messages<'a>( - &'a self, - cx: &'a AppContext, - ) -> impl 'a + Iterator)> { + fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator { let buffer = self.buffer.read(cx); - let mut messages = self.messages.iter().peekable(); + let mut message_anchors = self.message_anchors.iter().enumerate().peekable(); iter::from_fn(move || { - while let Some(message) = messages.next() { - let metadata = self.messages_metadata.get(&message.id)?; - let message_start = message.start.to_offset(buffer); + while let Some((start_ix, message_anchor)) = message_anchors.next() { + let metadata = self.messages_metadata.get(&message_anchor.id)?; + let message_start = message_anchor.start.to_offset(buffer); let mut message_end = None; - while let Some(next_message) = messages.peek() { + let mut end_ix = start_ix; + while let Some((_, next_message)) = message_anchors.peek() { if next_message.start.is_valid(buffer) { message_end = Some(next_message.start); break; } else { - messages.next(); + end_ix += 1; + message_anchors.next(); } } let message_end = message_end .unwrap_or(language::Anchor::MAX) .to_offset(buffer); - return Some((message, metadata, message_start..message_end)); + return Some(Message { + index_range: start_ix..end_ix, + offset_range: message_start..message_end, + id: message_anchor.id, + anchor: message_anchor.start, + role: metadata.role, + sent_at: metadata.sent_at, + status: metadata.status.clone(), + }); } None }) } + + fn save( + &mut self, + debounce: Option, + fs: Arc, + cx: &mut ModelContext, + ) { + self.pending_save = cx.spawn(|this, mut cx| async move { + if let Some(debounce) = debounce { + cx.background().timer(debounce).await; + } + + let (old_path, summary) = this.read_with(&cx, |this, _| { + let path = this.path.clone(); + let summary = if let Some(summary) = this.summary.as_ref() { + if summary.done { + Some(summary.text.clone()) + } else { + None + } + } else { + None + }; + (path, summary) + }); + + if let Some(summary) = summary { + let conversation = this.read_with(&cx, |this, cx| this.serialize(cx)); + let path = if let Some(old_path) = old_path { + old_path + } else { + let mut discriminant = 1; + let mut new_path; + loop { + new_path = CONVERSATIONS_DIR.join(&format!( + "{} - {}.zed.json", + summary.trim(), + discriminant + )); + if fs.is_file(&new_path).await { + discriminant += 1; + } else { + break; + } + } + new_path + }; + + fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; + fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) + .await?; + this.update(&mut cx, |this, _| this.path = Some(path)); + } + + Ok(()) + }); + } } struct PendingCompletion { id: usize, - _task: Task<()>, + _tasks: Vec>, } -enum AssistantEditorEvent { +enum ConversationEditorEvent { TabContentChanged, } @@ -816,39 +1598,50 @@ struct ScrollPosition { cursor: Anchor, } -struct AssistantEditor { - assistant: ModelHandle, +struct ConversationEditor { + conversation: ModelHandle, + fs: Arc, editor: ViewHandle, blocks: HashSet, scroll_position: Option, _subscriptions: Vec, } -impl AssistantEditor { +impl ConversationEditor { fn new( api_key: Rc>>, language_registry: Arc, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { + let conversation = cx.add_model(|cx| Conversation::new(api_key, language_registry, cx)); + Self::for_conversation(conversation, fs, cx) + } + + fn for_conversation( + conversation: ModelHandle, + fs: Arc, cx: &mut ViewContext, ) -> Self { - let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx)); let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(assistant.read(cx).buffer.clone(), None, cx); + let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); editor }); let _subscriptions = vec![ - cx.observe(&assistant, |_, _, cx| cx.notify()), - cx.subscribe(&assistant, Self::handle_assistant_event), + cx.observe(&conversation, |_, _, cx| cx.notify()), + cx.subscribe(&conversation, Self::handle_conversation_event), cx.subscribe(&editor, Self::handle_editor_event), ]; let mut this = Self { - assistant, + conversation, editor, blocks: Default::default(), scroll_position: None, + fs, _subscriptions, }; this.update_message_headers(cx); @@ -856,60 +1649,87 @@ impl AssistantEditor { } fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { - let user_message = self.assistant.update(cx, |assistant, cx| { - let editor = self.editor.read(cx); - let newest_selection = editor - .selections - .newest_anchor() - .head() - .to_offset(&editor.buffer().read(cx).snapshot(cx)); - let message_id = assistant.message_id_for_offset(newest_selection, cx)?; - let metadata = assistant.messages_metadata.get(&message_id)?; - let user_message = if metadata.role == Role::User { - let (_, user_message) = assistant.assist(cx)?; - user_message - } else { - let user_message = assistant.insert_message_after(message_id, Role::User, cx)?; - user_message - }; - Some(user_message) - }); + let cursors = self.cursors(cx); - if let Some(user_message) = user_message { - let cursor = user_message - .start - .to_offset(&self.assistant.read(cx).buffer.read(cx)); + let user_messages = self.conversation.update(cx, |conversation, cx| { + let selected_messages = conversation + .messages_for_offsets(cursors, cx) + .into_iter() + .map(|message| message.id) + .collect(); + conversation.assist(selected_messages, cx) + }); + let new_selections = user_messages + .iter() + .map(|message| { + let cursor = message + .start + .to_offset(self.conversation.read(cx).buffer.read(cx)); + cursor..cursor + }) + .collect::>(); + if !new_selections.is_empty() { self.editor.update(cx, |editor, cx| { editor.change_selections( Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), cx, - |selections| selections.select_ranges([cursor..cursor]), + |selections| selections.select_ranges(new_selections), ); }); + // Avoid scrolling to the new cursor position so the assistant's output is stable. + cx.defer(|this, _| this.scroll_position = None); } } fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { if !self - .assistant - .update(cx, |assistant, _| assistant.cancel_last_assist()) + .conversation + .update(cx, |conversation, _| conversation.cancel_last_assist()) { cx.propagate_action(); } } - fn handle_assistant_event( + fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext) { + let cursors = self.cursors(cx); + self.conversation.update(cx, |conversation, cx| { + let messages = conversation + .messages_for_offsets(cursors, cx) + .into_iter() + .map(|message| message.id) + .collect(); + conversation.cycle_message_roles(messages, cx) + }); + } + + fn cursors(&self, cx: &AppContext) -> Vec { + let selections = self.editor.read(cx).selections.all::(cx); + selections + .into_iter() + .map(|selection| selection.head()) + .collect() + } + + fn handle_conversation_event( &mut self, - _: ModelHandle, - event: &AssistantEvent, + _: ModelHandle, + event: &ConversationEvent, cx: &mut ViewContext, ) { match event { - AssistantEvent::MessagesEdited => self.update_message_headers(cx), - AssistantEvent::SummaryChanged => { - cx.emit(AssistantEditorEvent::TabContentChanged); + ConversationEvent::MessagesEdited => { + self.update_message_headers(cx); + self.conversation.update(cx, |conversation, cx| { + conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); + }); } - AssistantEvent::StreamedCompletion => { + ConversationEvent::SummaryChanged => { + cx.emit(ConversationEditorEvent::TabContentChanged); + self.conversation.update(cx, |conversation, cx| { + conversation.save(None, self.fs.clone(), cx); + }); + } + ConversationEvent::StreamedCompletion => { self.editor.update(cx, |editor, cx| { if let Some(scroll_position) = self.scroll_position { let snapshot = editor.snapshot(cx); @@ -979,17 +1799,17 @@ impl AssistantEditor { let excerpt_id = *buffer.as_singleton().unwrap().0; let old_blocks = std::mem::take(&mut self.blocks); let new_blocks = self - .assistant + .conversation .read(cx) .messages(cx) - .map(|(message, metadata, _)| BlockProperties { - position: buffer.anchor_in_excerpt(excerpt_id, message.start), + .map(|message| BlockProperties { + position: buffer.anchor_in_excerpt(excerpt_id, message.anchor), height: 2, style: BlockStyle::Sticky, render: Arc::new({ - let assistant = self.assistant.clone(); - let metadata = metadata.clone(); - let message = message.clone(); + let conversation = self.conversation.clone(); + // let metadata = message.metadata.clone(); + // let message = message.clone(); move |cx| { enum Sender {} enum ErrorTooltip {} @@ -1000,21 +1820,21 @@ impl AssistantEditor { let sender = MouseEventHandler::::new( message_id.0, cx, - |state, _| match metadata.role { + |state, _| match message.role { Role::User => { - let style = style.user_sender.style_for(state, false); + let style = style.user_sender.style_for(state); Label::new("You", style.text.clone()) .contained() .with_style(style.container) } Role::Assistant => { - let style = style.assistant_sender.style_for(state, false); + let style = style.assistant_sender.style_for(state); Label::new("Assistant", style.text.clone()) .contained() .with_style(style.container) } Role::System => { - let style = style.system_sender.style_for(state, false); + let style = style.system_sender.style_for(state); Label::new("System", style.text.clone()) .contained() .with_style(style.container) @@ -1023,10 +1843,13 @@ impl AssistantEditor { ) .with_cursor_style(CursorStyle::PointingHand) .on_down(MouseButton::Left, { - let assistant = assistant.clone(); + let conversation = conversation.clone(); move |_, _, cx| { - assistant.update(cx, |assistant, cx| { - assistant.cycle_message_role(message_id, cx) + conversation.update(cx, |conversation, cx| { + conversation.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) }) } }); @@ -1035,33 +1858,39 @@ impl AssistantEditor { .with_child(sender.aligned()) .with_child( Label::new( - metadata.sent_at.format("%I:%M%P").to_string(), + message.sent_at.format("%I:%M%P").to_string(), style.sent_at.text.clone(), ) .contained() .with_style(style.sent_at.container) .aligned(), ) - .with_children(metadata.error.clone().map(|error| { - Svg::new("icons/circle_x_mark_12.svg") - .with_color(style.error_icon.color) - .constrained() - .with_width(style.error_icon.width) - .contained() - .with_style(style.error_icon.container) - .with_tooltip::( - message_id.0, - error, - None, - theme.tooltip.clone(), - cx, + .with_children( + if let MessageStatus::Error(error) = &message.status { + Some( + Svg::new("icons/circle_x_mark_12.svg") + .with_color(style.error_icon.color) + .constrained() + .with_width(style.error_icon.width) + .contained() + .with_style(style.error_icon.container) + .with_tooltip::( + message_id.0, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), ) - .aligned() - })) + } else { + None + }, + ) .aligned() .left() .contained() - .with_style(style.header) + .with_style(style.message_header) .into_any() } }), @@ -1083,7 +1912,7 @@ impl AssistantEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::()) else { + let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::(cx)) else { return; }; @@ -1122,41 +1951,36 @@ impl AssistantEditor { if let Some(text) = text { panel.update(cx, |panel, cx| { - if let Some(assistant) = panel - .pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - .ok_or_else(|| anyhow!("no active context")) - .log_err() - { - assistant.update(cx, |assistant, cx| { - assistant - .editor - .update(cx, |editor, cx| editor.insert(&text, cx)) - }); - } + let conversation = panel + .active_editor() + .cloned() + .unwrap_or_else(|| panel.new_conversation(cx)); + conversation.update(cx, |conversation, cx| { + conversation + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); }); } } fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { let editor = self.editor.read(cx); - let assistant = self.assistant.read(cx); + let conversation = self.conversation.read(cx); if editor.selections.count() == 1 { let selection = editor.selections.newest::(cx); let mut copied_text = String::new(); let mut spanned_messages = 0; - for (_message, metadata, message_range) in assistant.messages(cx) { - if message_range.start >= selection.range().end { + for message in conversation.messages(cx) { + if message.offset_range.start >= selection.range().end { break; - } else if message_range.end >= selection.range().start { - let range = cmp::max(message_range.start, selection.range().start) - ..cmp::min(message_range.end, selection.range().end); + } else if message.offset_range.end >= selection.range().start { + let range = cmp::max(message.offset_range.start, selection.range().start) + ..cmp::min(message.offset_range.end, selection.range().end); if !range.is_empty() { spanned_messages += 1; - write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap(); - for chunk in assistant.buffer.read(cx).text_for_range(range) { + write!(&mut copied_text, "## {}\n\n", message.role).unwrap(); + for chunk in conversation.buffer.read(cx).text_for_range(range) { copied_text.push_str(&chunk); } copied_text.push('\n'); @@ -1174,53 +1998,94 @@ impl AssistantEditor { cx.propagate_action(); } + fn split(&mut self, _: &Split, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + let selections = self.editor.read(cx).selections.disjoint_anchors(); + for selection in selections.into_iter() { + let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); + let range = selection + .map(|endpoint| endpoint.to_offset(&buffer)) + .range(); + conversation.split_message(range, cx); + } + }); + } + + fn save(&mut self, _: &Save, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + conversation.save(None, self.fs.clone(), cx) + }); + } + fn cycle_model(&mut self, cx: &mut ViewContext) { - self.assistant.update(cx, |assistant, cx| { - let new_model = match assistant.model.as_str() { - "gpt-4" => "gpt-3.5-turbo", - _ => "gpt-4", + self.conversation.update(cx, |conversation, cx| { + let new_model = match conversation.model.as_str() { + "gpt-4-0613" => "gpt-3.5-turbo-0613", + _ => "gpt-4-0613", }; - assistant.set_model(new_model.into(), cx); + conversation.set_model(new_model.into(), cx); }); } fn title(&self, cx: &AppContext) -> String { - self.assistant + self.conversation .read(cx) .summary - .clone() - .unwrap_or_else(|| "New Context".into()) - } -} - -impl Entity for AssistantEditor { - type Event = AssistantEditorEvent; -} - -impl View for AssistantEditor { - fn ui_name() -> &'static str { - "AssistantEditor" + .as_ref() + .map(|summary| summary.text.clone()) + .unwrap_or_else(|| "New Conversation".into()) } - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + fn render_current_model( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> impl Element { enum Model {} - let theme = &theme::current(cx).assistant; - let assistant = &self.assistant.read(cx); - let model = assistant.model.clone(); - let remaining_tokens = assistant.remaining_tokens().map(|remaining_tokens| { - let remaining_tokens_style = if remaining_tokens <= 0 { - &theme.no_remaining_tokens - } else { - &theme.remaining_tokens - }; + + MouseEventHandler::::new(0, cx, |state, cx| { + let style = style.model.style_for(state); + Label::new(self.conversation.read(cx).model.clone(), style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) + } + + fn render_remaining_tokens( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> Option> { + let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; + let remaining_tokens_style = if remaining_tokens <= 0 { + &style.no_remaining_tokens + } else { + &style.remaining_tokens + }; + Some( Label::new( remaining_tokens.to_string(), remaining_tokens_style.text.clone(), ) .contained() - .with_style(remaining_tokens_style.container) - }); + .with_style(remaining_tokens_style.container), + ) + } +} +impl Entity for ConversationEditor { + type Event = ConversationEditorEvent; +} + +impl View for ConversationEditor { + fn ui_name() -> &'static str { + "ConversationEditor" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &theme::current(cx).assistant; Stack::new() .with_child( ChildView::new(&self.editor, cx) @@ -1229,19 +2094,8 @@ impl View for AssistantEditor { ) .with_child( Flex::row() - .with_child( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.model.style_for(state, false); - Label::new(model, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)), - ) - .with_children(remaining_tokens) - .contained() - .with_style(theme.model_info_container) + .with_child(self.render_current_model(theme, cx)) + .with_children(self.render_remaining_tokens(theme, cx)) .aligned() .top() .right(), @@ -1256,43 +2110,32 @@ impl View for AssistantEditor { } } -impl Item for AssistantEditor { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - cx: &gpui::AppContext, - ) -> AnyElement { - let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN); - Label::new(title, style.label.clone()).into_any() - } - - fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { - Some(self.title(cx).into()) - } - - fn as_searchable( - &self, - _: &ViewHandle, - ) -> Option> { - Some(Box::new(self.editor.clone())) - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)] -struct MessageId(usize); - #[derive(Clone, Debug)] -struct Message { +struct MessageAnchor { id: MessageId, start: language::Anchor, } #[derive(Clone, Debug)] -struct MessageMetadata { +pub struct Message { + offset_range: Range, + index_range: Range, + id: MessageId, + anchor: language::Anchor, role: Role, sent_at: DateTime, - error: Option, + status: MessageStatus, +} + +impl Message { + fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { + let mut content = format!("[Message {}]\n", self.id.0).to_string(); + content.extend(buffer.text_for_range(self.offset_range.clone())); + RequestMessage { + role: self.role, + content: content.trim_end().into(), + } + } } async fn stream_completion( @@ -1384,27 +2227,28 @@ async fn stream_completion( #[cfg(test)] mod tests { use super::*; + use crate::MessageId; use gpui::AppContext; #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { let registry = Arc::new(LanguageRegistry::test()); - let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx)); - let buffer = assistant.read(cx).buffer.clone(); + let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); + let buffer = conversation.read(cx).buffer.clone(); - let message_1 = assistant.read(cx).messages[0].clone(); + let message_1 = conversation.read(cx).message_anchors[0].clone(); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![(message_1.id, Role::User, 0..0)] ); - let message_2 = assistant.update(cx, |assistant, cx| { - assistant - .insert_message_after(message_1.id, Role::Assistant, cx) + let message_2 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..1), (message_2.id, Role::Assistant, 1..1) @@ -1415,20 +2259,20 @@ mod tests { buffer.edit([(0..0, "1"), (1..1, "2")], None, cx) }); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..3) ] ); - let message_3 = assistant.update(cx, |assistant, cx| { - assistant - .insert_message_after(message_2.id, Role::User, cx) + let message_3 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -1436,13 +2280,13 @@ mod tests { ] ); - let message_4 = assistant.update(cx, |assistant, cx| { - assistant - .insert_message_after(message_2.id, Role::User, cx) + let message_4 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -1455,7 +2299,7 @@ mod tests { buffer.edit([(4..4, "C"), (5..5, "D")], None, cx) }); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -1467,7 +2311,7 @@ mod tests { // Deleting across message boundaries merges the messages. buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx)); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..3), (message_3.id, Role::User, 3..4), @@ -1477,7 +2321,7 @@ mod tests { // Undoing the deletion should also undo the merge. buffer.update(cx, |buffer, cx| buffer.undo(cx)); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..2), (message_2.id, Role::Assistant, 2..4), @@ -1489,7 +2333,7 @@ mod tests { // Redoing the deletion should also redo the merge. buffer.update(cx, |buffer, cx| buffer.redo(cx)); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..3), (message_3.id, Role::User, 3..4), @@ -1497,13 +2341,13 @@ mod tests { ); // Ensure we can still insert after a merged message. - let message_5 = assistant.update(cx, |assistant, cx| { - assistant - .insert_message_after(message_1.id, Role::System, cx) + let message_5 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) .unwrap() }); assert_eq!( - messages(&assistant, cx), + messages(&conversation, cx), vec![ (message_1.id, Role::User, 0..3), (message_5.id, Role::System, 3..4), @@ -1512,14 +2356,246 @@ mod tests { ); } + #[gpui::test] + fn test_message_splitting(cx: &mut AppContext) { + let registry = Arc::new(LanguageRegistry::test()); + let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx) + }); + + let (_, message_2) = + conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); + let message_2 = message_2.unwrap(); + + // We recycle newlines in the middle of a split message + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..16), + ] + ); + + let (_, message_3) = + conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); + let message_3 = message_3.unwrap(); + + // We don't recycle newlines at the end of a split message + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..17), + ] + ); + + let (_, message_4) = + conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); + let message_4 = message_4.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..17), + ] + ); + + let (_, message_5) = + conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); + let message_5 = message_5.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..10), + (message_5.id, Role::User, 10..18), + ] + ); + + let (message_6, message_7) = conversation.update(cx, |conversation, cx| { + conversation.split_message(14..16, cx) + }); + let message_6 = message_6.unwrap(); + let message_7 = message_7.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..10), + (message_5.id, Role::User, 10..14), + (message_6.id, Role::User, 14..17), + (message_7.id, Role::User, 17..19), + ] + ); + } + + #[gpui::test] + fn test_messages_for_offsets(cx: &mut AppContext) { + let registry = Arc::new(LanguageRegistry::test()); + let conversation = cx.add_model(|cx| Conversation::new(Default::default(), registry, cx)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx)); + let message_2 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx)); + + let message_3 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx)); + + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..8), + (message_3.id, Role::User, 8..11) + ] + ); + + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 4, 9], cx), + [message_1.id, message_2.id, message_3.id] + ); + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 1, 11], cx), + [message_1.id, message_3.id] + ); + + let message_4 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..8), + (message_3.id, Role::User, 8..12), + (message_4.id, Role::User, 12..12) + ] + ); + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx), + [message_1.id, message_2.id, message_3.id, message_4.id] + ); + + fn message_ids_for_offsets( + conversation: &ModelHandle, + offsets: &[usize], + cx: &AppContext, + ) -> Vec { + conversation + .read(cx) + .messages_for_offsets(offsets.iter().copied(), cx) + .into_iter() + .map(|message| message.id) + .collect() + } + } + + #[gpui::test] + fn test_serialization(cx: &mut AppContext) { + let registry = Arc::new(LanguageRegistry::test()); + let conversation = + cx.add_model(|cx| Conversation::new(Default::default(), registry.clone(), cx)); + let buffer = conversation.read(cx).buffer.clone(); + let message_0 = conversation.read(cx).message_anchors[0].id; + let message_1 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx) + .unwrap() + }); + let message_2 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx); + buffer.finalize_last_transaction(); + }); + let _message_3 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_eq!(buffer.read(cx).text(), "a\nb\nc\n"); + assert_eq!( + messages(&conversation, cx), + [ + (message_0, Role::User, 0..2), + (message_1.id, Role::Assistant, 2..6), + (message_2.id, Role::System, 6..6), + ] + ); + + let deserialized_conversation = cx.add_model(|cx| { + Conversation::deserialize( + conversation.read(cx).serialize(cx), + Default::default(), + Default::default(), + registry.clone(), + cx, + ) + }); + let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone(); + assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n"); + assert_eq!( + messages(&deserialized_conversation, cx), + [ + (message_0, Role::User, 0..2), + (message_1.id, Role::Assistant, 2..6), + (message_2.id, Role::System, 6..6), + ] + ); + } + fn messages( - assistant: &ModelHandle, + conversation: &ModelHandle, cx: &AppContext, ) -> Vec<(MessageId, Role, Range)> { - assistant + conversation .read(cx) .messages(cx) - .map(|(message, metadata, range)| (message.id, metadata.role, range)) + .map(|message| (message.id, message.role, message.offset_range)) .collect() } } diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml new file mode 100644 index 0000000000..182e421eb8 --- /dev/null +++ b/crates/audio/Cargo.toml @@ -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] diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs new file mode 100644 index 0000000000..b58e1f6aee --- /dev/null +++ b/crates/audio/src/assets.rs @@ -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>>, f32>>; + +pub struct SoundRegistry { + cache: Arc>>, + assets: Box, +} + +impl SoundRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + cache: Default::default(), + assets: Box::new(source), + }) + } + + pub fn global(cx: &AppContext) -> Arc { + cx.global::>().clone() + } + + pub fn get(&self, name: &str) -> Result> { + 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::().buffered(); + + self.cache.lock().insert(name.to_string(), source.clone()); + + Ok(source) + } +} diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs new file mode 100644 index 0000000000..233b0f62aa --- /dev/null +++ b/crates/audio/src/audio.rs @@ -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, + output_handle: Option, +} + +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::() { + return; + } + + let this = cx.global::(); + + 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(); + } +} diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 6f31df614d..cd2e53905d 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -49,7 +49,7 @@ impl View for UpdateNotification { ) .with_child( MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state, false); + let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) .constrained() @@ -74,7 +74,7 @@ impl View for UpdateNotification { ), ) .with_child({ - let style = theme.action_message.style_for(state, false); + let style = theme.action_message.style_for(state); Text::new("View the release notes", style.text.clone()) .contained() .with_style(style.container) diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 906d70abb7..433dbed29b 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -83,7 +83,7 @@ impl View for Breadcrumbs { } MouseEventHandler::::new(0, cx, |state, _| { - let style = style.style_for(state, false); + let style = style.style_for(state); crumbs.with_style(style.container) }) .on_click(MouseButton::Left, |_, this, cx| { diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 4e83b552fb..61f3593247 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ ] [dependencies] +audio = { path = "../audio" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/participant.rs b/crates/call/src/participant.rs index 4b9e7ba034..e7858869ce 100644 --- a/crates/call/src/participant.rs +++ b/crates/call/src/participant.rs @@ -3,6 +3,7 @@ use client::{proto, User}; use collections::HashMap; use gpui::WeakModelHandle; pub use live_kit_client::Frame; +use live_kit_client::RemoteAudioTrack; use project::Project; use std::{fmt, sync::Arc}; @@ -42,7 +43,10 @@ pub struct RemoteParticipant { pub peer_id: proto::PeerId, pub projects: Vec, pub location: ParticipantLocation, - pub tracks: HashMap>, + pub muted: bool, + pub speaking: bool, + pub video_tracks: HashMap>, + pub audio_tracks: HashMap>, } #[derive(Clone)] diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 775342359f..87e6faf988 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -3,6 +3,7 @@ use crate::{ IncomingCall, }; use anyhow::{anyhow, Result}; +use audio::{Audio, Sound}; use client::{ proto::{self, PeerId}, Client, TypedEnvelope, User, UserStore, @@ -12,7 +13,10 @@ use fs::Fs; use futures::{FutureExt, StreamExt}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use language::LanguageRegistry; -use live_kit_client::{LocalTrackPublication, LocalVideoTrack, RemoteVideoTrackUpdate}; +use live_kit_client::{ + LocalAudioTrack, LocalTrackPublication, LocalVideoTrack, RemoteAudioTrackUpdate, + RemoteVideoTrackUpdate, +}; use postage::stream::Stream; use project::Project; use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; @@ -28,6 +32,9 @@ pub enum Event { RemoteVideoTracksChanged { participant_id: proto::PeerId, }, + RemoteAudioTracksChanged { + participant_id: proto::PeerId, + }, RemoteProjectShared { owner: Arc, project_id: u64, @@ -112,9 +119,9 @@ impl Room { } }); - let mut track_changes = room.remote_video_track_updates(); - let _maintain_tracks = cx.spawn_weak(|this, mut cx| async move { - while let Some(track_change) = track_changes.next().await { + let mut track_video_changes = room.remote_video_track_updates(); + let _maintain_video_tracks = cx.spawn_weak(|this, mut cx| async move { + while let Some(track_change) = track_video_changes.next().await { let this = if let Some(this) = this.upgrade(&cx) { this } else { @@ -127,16 +134,42 @@ impl Room { } }); - cx.foreground() - .spawn(room.connect(&connection_info.server_url, &connection_info.token)) - .detach_and_log_err(cx); + let mut track_audio_changes = room.remote_audio_track_updates(); + let _maintain_audio_tracks = cx.spawn_weak(|this, mut cx| async move { + while let Some(track_change) = track_audio_changes.next().await { + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; + + this.update(&mut cx, |this, cx| { + this.remote_audio_track_updated(track_change, cx).log_err() + }); + } + }); + + 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?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); Some(LiveKitRoom { room, - screen_track: ScreenTrack::None, + screen_track: LocalTrack::None, + microphone_track: LocalTrack::None, next_publish_id: 0, + muted_by_user: false, + deafened: false, + speaking: false, _maintain_room, - _maintain_tracks, + _maintain_tracks: [_maintain_video_tracks, _maintain_audio_tracks], }) } else { None @@ -145,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, @@ -234,6 +269,7 @@ impl Room { room.apply_room_update(room_proto, cx)?; anyhow::Ok(()) })?; + Ok(room) }) } @@ -275,6 +311,8 @@ impl Room { } } + Audio::play_sound(Sound::Leave, cx); + self.status = RoomStatus::Offline; self.remote_participants.clear(); self.pending_participants.clear(); @@ -618,20 +656,34 @@ impl Room { peer_id, projects: participant.projects, location, - tracks: Default::default(), + muted: false, + speaking: false, + video_tracks: Default::default(), + audio_tracks: Default::default(), }, ); + Audio::play_sound(Sound::Joined, cx); + if let Some(live_kit) = this.live_kit.as_ref() { - let tracks = + let video_tracks = live_kit.room.remote_video_tracks(&user.id.to_string()); - for track in tracks { + let audio_tracks = + live_kit.room.remote_audio_tracks(&user.id.to_string()); + for track in video_tracks { this.remote_video_track_updated( RemoteVideoTrackUpdate::Subscribed(track), cx, ) .log_err(); } + for track in audio_tracks { + this.remote_audio_track_updated( + RemoteAudioTrackUpdate::Subscribed(track), + cx, + ) + .log_err(); + } } } } @@ -706,7 +758,7 @@ impl Room { .remote_participants .get_mut(&user_id) .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; - participant.tracks.insert( + participant.video_tracks.insert( track_id.clone(), Arc::new(RemoteVideoTrack { live_kit_track: track, @@ -725,7 +777,7 @@ impl Room { .remote_participants .get_mut(&user_id) .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; - participant.tracks.remove(&track_id); + participant.video_tracks.remove(&track_id); cx.emit(Event::RemoteVideoTracksChanged { participant_id: participant.peer_id, }); @@ -736,6 +788,84 @@ impl Room { Ok(()) } + fn remote_audio_track_updated( + &mut self, + change: RemoteAudioTrackUpdate, + cx: &mut ModelContext, + ) -> Result<()> { + match change { + RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } => { + let mut speaker_ids = speakers + .into_iter() + .filter_map(|speaker_sid| speaker_sid.parse().ok()) + .collect::>(); + speaker_ids.sort_unstable(); + for (sid, participant) in &mut self.remote_participants { + if let Ok(_) = speaker_ids.binary_search(sid) { + participant.speaking = true; + } else { + participant.speaking = false; + } + } + if let Some(id) = self.client.user_id() { + if let Some(room) = &mut self.live_kit { + if let Ok(_) = speaker_ids.binary_search(&id) { + room.speaking = true; + } else { + room.speaking = false; + } + } + } + cx.notify(); + } + RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => { + for participant in &mut self.remote_participants.values_mut() { + let mut found = false; + for track in participant.audio_tracks.values() { + if track.sid() == track_id { + found = true; + break; + } + } + if found { + participant.muted = muted; + break; + } + } + cx.notify(); + } + RemoteAudioTrackUpdate::Subscribed(track) => { + let user_id = track.publisher_id().parse()?; + let track_id = track.sid().to_string(); + let participant = self + .remote_participants + .get_mut(&user_id) + .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; + participant.audio_tracks.insert(track_id.clone(), track); + cx.emit(Event::RemoteAudioTracksChanged { + participant_id: participant.peer_id, + }); + } + RemoteAudioTrackUpdate::Unsubscribed { + publisher_id, + track_id, + } => { + let user_id = publisher_id.parse()?; + let participant = self + .remote_participants + .get_mut(&user_id) + .ok_or_else(|| anyhow!("unsubscribed from track by unknown participant"))?; + participant.audio_tracks.remove(&track_id); + cx.emit(Event::RemoteAudioTracksChanged { + participant_id: participant.peer_id, + }); + } + } + + cx.notify(); + Ok(()) + } + fn check_invariants(&self) { #[cfg(any(test, feature = "test-support"))] { @@ -801,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) { @@ -908,7 +1039,116 @@ impl Room { pub fn is_screen_sharing(&self) -> bool { self.live_kit.as_ref().map_or(false, |live_kit| { - !matches!(live_kit.screen_track, ScreenTrack::None) + !matches!(live_kit.screen_track, LocalTrack::None) + }) + } + + pub fn is_sharing_mic(&self) -> bool { + self.live_kit.as_ref().map_or(false, |live_kit| { + !matches!(live_kit.microphone_track, LocalTrack::None) + }) + } + + pub fn is_muted(&self) -> bool { + self.live_kit + .as_ref() + .and_then(|live_kit| match &live_kit.microphone_track { + LocalTrack::None => None, + LocalTrack::Pending { muted, .. } => Some(*muted), + LocalTrack::Published { muted, .. } => Some(*muted), + }) + .unwrap_or(false) + } + + pub fn is_speaking(&self) -> bool { + self.live_kit + .as_ref() + .map_or(false, |live_kit| live_kit.speaking) + } + + pub fn is_deafened(&self) -> Option { + self.live_kit.as_ref().map(|live_kit| live_kit.deafened) + } + + pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { + if self.status.is_offline() { + return Task::ready(Err(anyhow!("room is offline"))); + } else if self.is_sharing_mic() { + return Task::ready(Err(anyhow!("microphone was already shared"))); + } + + let publish_id = if let Some(live_kit) = self.live_kit.as_mut() { + let publish_id = post_inc(&mut live_kit.next_publish_id); + live_kit.microphone_track = LocalTrack::Pending { + publish_id, + muted: false, + }; + cx.notify(); + publish_id + } else { + return Task::ready(Err(anyhow!("live-kit was not initialized"))); + }; + + cx.spawn_weak(|this, mut cx| async move { + let publish_track = async { + let track = LocalAudioTrack::create(); + this.upgrade(&cx) + .ok_or_else(|| anyhow!("room was dropped"))? + .read_with(&cx, |this, _| { + this.live_kit + .as_ref() + .map(|live_kit| live_kit.room.publish_audio_track(&track)) + }) + .ok_or_else(|| anyhow!("live-kit was not initialized"))? + .await + }; + + let publication = publish_track.await; + this.upgrade(&cx) + .ok_or_else(|| anyhow!("room was dropped"))? + .update(&mut cx, |this, cx| { + let live_kit = this + .live_kit + .as_mut() + .ok_or_else(|| anyhow!("live-kit was not initialized"))?; + + let (canceled, muted) = if let LocalTrack::Pending { + publish_id: cur_publish_id, + muted, + } = &live_kit.microphone_track + { + (*cur_publish_id != publish_id, *muted) + } else { + (true, false) + }; + + match publication { + Ok(publication) => { + if canceled { + live_kit.room.unpublish_track(publication); + } else { + if muted { + cx.background().spawn(publication.set_mute(muted)).detach(); + } + live_kit.microphone_track = LocalTrack::Published { + track_publication: publication, + muted, + }; + cx.notify(); + } + Ok(()) + } + Err(error) => { + if canceled { + Ok(()) + } else { + live_kit.microphone_track = LocalTrack::None; + cx.notify(); + Err(error) + } + } + } + }) }) } @@ -921,7 +1161,10 @@ impl Room { let (displays, publish_id) = if let Some(live_kit) = self.live_kit.as_mut() { let publish_id = post_inc(&mut live_kit.next_publish_id); - live_kit.screen_track = ScreenTrack::Pending { publish_id }; + live_kit.screen_track = LocalTrack::Pending { + publish_id, + muted: false, + }; cx.notify(); (live_kit.room.display_sources(), publish_id) } else { @@ -955,13 +1198,14 @@ impl Room { .as_mut() .ok_or_else(|| anyhow!("live-kit was not initialized"))?; - let canceled = if let ScreenTrack::Pending { + let (canceled, muted) = if let LocalTrack::Pending { publish_id: cur_publish_id, + muted, } = &live_kit.screen_track { - *cur_publish_id != publish_id + (*cur_publish_id != publish_id, *muted) } else { - true + (true, false) }; match publication { @@ -969,16 +1213,25 @@ impl Room { if canceled { live_kit.room.unpublish_track(publication); } else { - live_kit.screen_track = ScreenTrack::Published(publication); + if muted { + cx.background().spawn(publication.set_mute(muted)).detach(); + } + live_kit.screen_track = LocalTrack::Published { + track_publication: publication, + muted, + }; cx.notify(); } + + Audio::play_sound(Sound::StartScreenshare, cx); + Ok(()) } Err(error) => { if canceled { Ok(()) } else { - live_kit.screen_track = ScreenTrack::None; + live_kit.screen_track = LocalTrack::None; cx.notify(); Err(error) } @@ -988,6 +1241,59 @@ impl Room { }) } + pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> { + let should_mute = !self.is_muted(); + if let Some(live_kit) = self.live_kit.as_mut() { + let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?; + live_kit.muted_by_user = should_mute; + + 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")) + } + } + + pub fn toggle_deafen(&mut self, cx: &mut ModelContext) -> Result>> { + if let Some(live_kit) = self.live_kit.as_mut() { + (*live_kit).deafened = !live_kit.deafened; + + let mut tasks = Vec::with_capacity(self.remote_participants.len()); + // Context notification is sent within set_mute itself. + let mut mute_task = None; + // 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(live_kit.set_mute(live_kit.deafened, cx)?.0); + }; + for participant in self.remote_participants.values() { + for track in live_kit + .room + .remote_audio_track_publications(&participant.user.id.to_string()) + { + tasks.push(cx.foreground().spawn(track.set_enabled(!live_kit.deafened))); + } + } + + Ok(cx.foreground().spawn(async move { + if let Some(mute_task) = mute_task { + mute_task.await?; + } + for task in tasks { + task.await?; + } + Ok(()) + })) + } else { + Err(anyhow!("LiveKit not started")) + } + } + pub fn unshare_screen(&mut self, cx: &mut ModelContext) -> Result<()> { if self.status.is_offline() { return Err(anyhow!("room is offline")); @@ -998,14 +1304,18 @@ impl Room { .as_mut() .ok_or_else(|| anyhow!("live-kit was not initialized"))?; match mem::take(&mut live_kit.screen_track) { - ScreenTrack::None => Err(anyhow!("screen was not shared")), - ScreenTrack::Pending { .. } => { + LocalTrack::None => Err(anyhow!("screen was not shared")), + LocalTrack::Pending { .. } => { cx.notify(); Ok(()) } - ScreenTrack::Published(track) => { - live_kit.room.unpublish_track(track); + LocalTrack::Published { + track_publication, .. + } => { + live_kit.room.unpublish_track(track_publication); cx.notify(); + + Audio::play_sound(Sound::StopScreenshare, cx); Ok(()) } } @@ -1023,19 +1333,75 @@ impl Room { struct LiveKitRoom { room: Arc, - screen_track: ScreenTrack, + screen_track: LocalTrack, + microphone_track: LocalTrack, + /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user. + muted_by_user: bool, + deafened: bool, + speaking: bool, next_publish_id: usize, _maintain_room: Task<()>, - _maintain_tracks: Task<()>, + _maintain_tracks: [Task<()>; 2], } -enum ScreenTrack { +impl LiveKitRoom { + fn set_mute( + self: &mut LiveKitRoom, + should_mute: bool, + cx: &mut ModelContext, + ) -> Result<(Task>, 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 { publish_id: usize }, - Published(LocalTrackPublication), + Pending { + publish_id: usize, + muted: bool, + }, + Published { + track_publication: LocalTrackPublication, + muted: bool, + }, } -impl Default for ScreenTrack { +impl Default for LocalTrack { fn default() -> Self { Self::None } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 6a6a91b485..9c4e187dbc 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,5 +1,4 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; -use db::kvp::KEY_VALUE_STORE; use gpui::{executor::Background, serde_json, AppContext, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -8,7 +7,6 @@ use std::{env, io::Write, mem, path::PathBuf, sync::Arc, time::Duration}; use tempfile::NamedTempFile; use util::http::HttpClient; use util::{channel::ReleaseChannel, TryFutureExt}; -use uuid::Uuid; pub struct Telemetry { http_client: Arc, @@ -120,39 +118,15 @@ impl Telemetry { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } - pub fn start(self: &Arc) { - let this = self.clone(); - self.executor - .spawn( - async move { - let installation_id = - if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp("device_id") { - installation_id - } else { - let installation_id = Uuid::new_v4().to_string(); - KEY_VALUE_STORE - .write_kvp("device_id".to_string(), installation_id.clone()) - .await?; - installation_id - }; + pub fn start(self: &Arc, installation_id: Option) { + let mut state = self.state.lock(); + state.installation_id = installation_id.map(|id| id.into()); + let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); + drop(state); - let installation_id: Arc = installation_id.into(); - let mut state = this.state.lock(); - state.installation_id = Some(installation_id.clone()); - - let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); - - drop(state); - - if has_clickhouse_events { - this.flush_clickhouse_events(); - } - - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); + if has_clickhouse_events { + self.flush_clickhouse_events(); + } } /// This method takes the entire TelemetrySettings struct in order to force client code diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 27545a652d..c61fdeebfb 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.14.2" +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"] } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 595d841d07..c690b6148a 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -74,6 +74,7 @@ CREATE TABLE "worktree_entries" ( "mtime_seconds" INTEGER NOT NULL, "mtime_nanos" INTEGER NOT NULL, "is_symlink" BOOL NOT NULL, + "is_external" BOOL NOT NULL, "is_ignored" BOOL NOT NULL, "is_deleted" BOOL NOT NULL, "git_status" INTEGER, diff --git a/crates/collab/migrations/20230616134535_add_is_external_to_worktree_entries.sql b/crates/collab/migrations/20230616134535_add_is_external_to_worktree_entries.sql new file mode 100644 index 0000000000..e4348af0cc --- /dev/null +++ b/crates/collab/migrations/20230616134535_add_is_external_to_worktree_entries.sql @@ -0,0 +1,2 @@ +ALTER TABLE "worktree_entries" +ADD "is_external" BOOL NOT NULL DEFAULT FALSE; diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 7e2c376bc2..208da22efe 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1539,6 +1539,7 @@ impl Database { }), is_symlink: db_entry.is_symlink, is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, git_status: db_entry.git_status.map(|status| status as i32), }); } @@ -2349,6 +2350,7 @@ impl Database { mtime_nanos: ActiveValue::set(mtime.nanos as i32), is_symlink: ActiveValue::set(entry.is_symlink), is_ignored: ActiveValue::set(entry.is_ignored), + is_external: ActiveValue::set(entry.is_external), git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), is_deleted: ActiveValue::set(false), scan_id: ActiveValue::set(update.scan_id as i64), @@ -2705,6 +2707,7 @@ impl Database { }), is_symlink: db_entry.is_symlink, is_ignored: db_entry.is_ignored, + is_external: db_entry.is_external, git_status: db_entry.git_status.map(|status| status as i32), }); } diff --git a/crates/collab/src/db/worktree_entry.rs b/crates/collab/src/db/worktree_entry.rs index f2df808ee3..cf5090ab6d 100644 --- a/crates/collab/src/db/worktree_entry.rs +++ b/crates/collab/src/db/worktree_entry.rs @@ -18,6 +18,7 @@ pub struct Model { pub git_status: Option, pub is_symlink: bool, pub is_ignored: bool, + pub is_external: bool, pub is_deleted: bool, pub scan_id: i64, } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 8d210513c2..14d785307d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -201,6 +201,7 @@ impl Server { .add_message_handler(update_language_server) .add_message_handler(update_diagnostic_summary) .add_message_handler(update_worktree_settings) + .add_message_handler(refresh_inlay_hints) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) @@ -224,7 +225,9 @@ impl Server { .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) + .add_request_handler(forward_project_request::) .add_request_handler(forward_project_request::) + .add_request_handler(forward_project_request::) .add_message_handler(create_buffer_for_peer) .add_request_handler(update_buffer) .add_message_handler(update_buffer_file) @@ -1573,6 +1576,10 @@ async fn update_worktree_settings( Ok(()) } +async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> { + broadcast_project_message(request.project_id, request, session).await +} + async fn start_language_server( request: proto::StartLanguageServer, session: Session, @@ -1749,7 +1756,15 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re } async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); + broadcast_project_message(request.project_id, request, session).await +} + +async fn broadcast_project_message( + project_id: u64, + request: T, + session: Session, +) -> Result<()> { + let project_id = ProjectId::from_proto(project_id); let project_connection_ids = session .db() .await diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index b51c5240a8..b1d0bedb2c 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -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); }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ce4ede8684..b20844a065 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -18,7 +18,7 @@ use gpui::{ }; use indoc::indoc; use language::{ - language_settings::{AllLanguageSettings, Formatter}, + language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings}, tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, LanguageConfig, OffsetRangeExt, Point, Rope, }; @@ -34,12 +34,17 @@ use std::{ path::{Path, PathBuf}, rc::Rc, sync::{ - atomic::{AtomicBool, Ordering::SeqCst}, + atomic::{AtomicBool, AtomicU32, Ordering::SeqCst}, Arc, }, }; use unindent::Unindent as _; -use workspace::{item::ItemHandle as _, shared_screen::SharedScreen, SplitDirection, Workspace}; +use workspace::{ + dock::{test::TestPanel, DockPosition}, + item::{test::TestItem, ItemHandle as _}, + shared_screen::SharedScreen, + SplitDirection, Workspace, +}; #[ctor::ctor] fn init_logger() { @@ -252,7 +257,7 @@ async fn test_basic_calls( room_b.read_with(cx_b, |room, _| { assert_eq!( room.remote_participants()[&client_a.user_id().unwrap()] - .tracks + .video_tracks .len(), 1 ); @@ -269,7 +274,7 @@ async fn test_basic_calls( room_c.read_with(cx_c, |room, _| { assert_eq!( room.remote_participants()[&client_a.user_id().unwrap()] - .tracks + .video_tracks .len(), 1 ); @@ -1261,6 +1266,27 @@ async fn test_share_project( let client_b_collaborator = project.collaborators().get(&client_b_peer_id).unwrap(); assert_eq!(client_b_collaborator.replica_id, replica_id_b); }); + project_b.read_with(cx_b, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap().read(cx); + assert_eq!( + worktree.paths().map(AsRef::as_ref).collect::>(), + [ + Path::new(".gitignore"), + Path::new("a.txt"), + Path::new("b.txt"), + Path::new("ignored-dir"), + ] + ); + }); + + project_b + .update(cx_b, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + let entry = worktree.read(cx).entry_for_path("ignored-dir").unwrap(); + project.expand_entry(worktree_id, entry.id, cx).unwrap() + }) + .await + .unwrap(); project_b.read_with(cx_b, |project, cx| { let worktree = project.worktrees(cx).next().unwrap().read(cx); assert_eq!( @@ -6847,12 +6873,43 @@ async fn test_basic_following( ) }); - // Client B activates an external window again, and the previously-opened screen-sharing item - // gets activated. - active_call_b - .update(cx_b, |call, cx| call.set_location(None, cx)) - .await - .unwrap(); + // Client B activates a panel, and the previously-opened screen-sharing item gets activated. + let panel = cx_b.add_view(workspace_b.window_id(), |_| { + TestPanel::new(DockPosition::Left) + }); + workspace_b.update(cx_b, |workspace, cx| { + workspace.add_panel(panel, cx); + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + shared_screen.id() + ); + + // Toggling the focus back to the pane causes client A to return to the multibuffer. + workspace_b.update(cx_b, |workspace, cx| { + workspace.toggle_panel_focus::(cx); + }); + deterministic.run_until_parked(); + workspace_a.read_with(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().id(), + multibuffer_editor_a.id() + ) + }); + + // Client B activates an item that doesn't implement following, + // so the previously-opened screen-sharing item gets activated. + let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new()); + workspace_b.update(cx_b, |workspace, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.add_item(Box::new(unfollowable_item), true, true, None, cx) + }) + }); deterministic.run_until_parked(); assert_eq!( workspace_a.read_with(cx_a, |workspace, cx| workspace @@ -6957,7 +7014,7 @@ async fn test_join_call_after_screen_was_shared( room.remote_participants() .get(&client_a.user_id().unwrap()) .unwrap() - .tracks + .video_tracks .len(), 1 ); @@ -7743,6 +7800,572 @@ async fn test_on_input_format_from_guest_to_host( }); } +#[gpui::test] +async fn test_mutual_editor_inlay_hint_cache_update( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry.add(Arc::clone(&language)); + client_b.language_registry.add(language); + + client_a + .fs + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a); + cx_a.foreground().start_waiting(); + + let _buffer_a = project_a + .update(cx_a, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); + let next_call_id = Arc::new(AtomicU32::new(0)); + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + fake_language_server + .handle_request::(move |params, _| { + let task_next_call_id = Arc::clone(&next_call_id); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); + let mut new_hints = Vec::with_capacity(current_call_id as usize); + loop { + new_hints.push(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, + }); + if current_call_id == 0 { + break; + } + current_call_id -= 1; + } + Ok(Some(new_hints)) + } + }) + .next() + .await + .unwrap(); + + cx_a.foreground().finish_waiting(); + cx_a.foreground().run_until_parked(); + + let mut edits_made = 1; + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec!["0".to_string()], + extract_hint_labels(editor), + "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", + ); + }); + let workspace_b = client_b.build_workspace(&project_b, cx_b); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + cx_b.foreground().run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["0".to_string(), "1".to_string()], + extract_hint_labels(editor), + "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" + ); + }); + + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone())); + editor.handle_input(":", cx); + cx.focus(&editor_b); + edits_made += 1; + }); + cx_a.foreground().run_until_parked(); + cx_b.foreground().run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec!["0".to_string(), "1".to_string(), "2".to_string()], + extract_hint_labels(editor), + "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, _| { + assert_eq!( + vec![ + "0".to_string(), + "1".to_string(), + "2".to_string(), + "3".to_string() + ], + extract_hint_labels(editor), + "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); + }); + + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("a change to increment both buffers' versions", cx); + cx.focus(&editor_a); + edits_made += 1; + }); + cx_a.foreground().run_until_parked(); + cx_b.foreground().run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![ + "0".to_string(), + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string() + ], + extract_hint_labels(editor), + "Host should get hints from 3rd edit, 5th LSP query: \ +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, _| { + assert_eq!( + vec![ + "0".to_string(), + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + "5".to_string(), + ], + extract_hint_labels(editor), + "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); + }); + + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + edits_made += 1; + cx_a.foreground().run_until_parked(); + cx_b.foreground().run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert_eq!( + vec![ + "0".to_string(), + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + "5".to_string(), + "6".to_string(), + ], + extract_hint_labels(editor), + "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" + ); + }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec![ + "0".to_string(), + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + "5".to_string(), + "6".to_string(), + "7".to_string(), + ], + extract_hint_labels(editor), + "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, + "Guest should accepted all edits and bump its cache version every time" + ); + }); +} + +#[gpui::test] +async fn test_inlay_hint_refresh_is_forwarded( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + server + .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) + .await; + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + + cx_a.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + cx_b.update(|cx| { + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: false, + show_other_hints: true, + }) + }); + }); + }); + let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + client_a.language_registry.add(Arc::clone(&language)); + client_b.language_registry.add(language); + + client_a + .fs + .insert_tree( + "/a", + json!({ + "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out", + "other.rs": "// Test file", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await; + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + let project_id = active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + + let project_b = client_b.build_remote_project(project_id, cx_b).await; + active_call_b + .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) + .await + .unwrap(); + + let workspace_a = client_a.build_workspace(&project_a, cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b); + cx_a.foreground().start_waiting(); + cx_b.foreground().start_waiting(); + + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + let fake_language_server = fake_language_servers.next().await.unwrap(); + let next_call_id = Arc::new(AtomicU32::new(0)); + fake_language_server + .handle_request::(move |params, _| { + let task_next_call_id = Arc::clone(&next_call_id); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); + let mut new_hints = Vec::with_capacity(current_call_id as usize); + loop { + new_hints.push(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, + }); + if current_call_id == 0 { + break; + } + current_call_id -= 1; + } + Ok(Some(new_hints)) + } + }) + .next() + .await + .unwrap(); + cx_a.foreground().finish_waiting(); + cx_b.foreground().finish_waiting(); + + cx_a.foreground().run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "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", + ); + }); + + let mut edits_made = 1; + cx_b.foreground().run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["0".to_string()], + extract_hint_labels(editor), + "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" + ); + }); + + fake_language_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx_a.foreground().run_until_parked(); + editor_a.update(cx_a, |editor, _| { + assert!( + extract_hint_labels(editor).is_empty(), + "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", + ); + }); + + edits_made += 1; + cx_b.foreground().run_until_parked(); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["0".to_string(), "1".to_string(),], + extract_hint_labels(editor), + "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" + ); + }); +} + #[derive(Debug, Eq, PartialEq)] struct RoomParticipants { remote: Vec, @@ -7766,3 +8389,17 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP RoomParticipants { remote, pending } }) } + +fn extract_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + for (_, inlay) in excerpt_hints.hints.iter() { + match &inlay.label { + project::InlayHintLabel::String(s) => labels.push(s.to_string()), + _ => unreachable!(), + } + } + } + labels +} diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 16cc6b5277..f81885c07a 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -35,10 +35,14 @@ 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" } util = { path = "../util" } workspace = { path = "../workspace" } +zed-actions = {path = "../zed-actions"} + anyhow.workspace = true futures.workspace = true diff --git a/crates/collab_ui/src/branch_list.rs b/crates/collab_ui/src/branch_list.rs new file mode 100644 index 0000000000..16fefbd2eb --- /dev/null +++ b/crates/collab_ui/src/branch_list.rs @@ -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::::init(cx); +} + +pub type BranchList = Picker; + +pub fn build_branch_list( + workspace: ViewHandle, + cx: &mut ViewContext, +) -> 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, + workspace: ViewHandle, + selected_index: usize, + last_query: String, +} + +impl PickerDelegate for BranchListDelegate { + fn placeholder_text(&self) -> Arc { + "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>) { + self.selected_index = ix; + } + + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> 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::>()) + }) + .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>) { + 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>) { + cx.emit(PickerEvent::Dismiss); + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> AnyElement> { + 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>, + ) -> Option>> { + 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()) + } +} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 720a73f477..73450e7c7d 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,6 +1,10 @@ use crate::{ - contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, - toggle_screen_sharing, ToggleScreenSharing, + 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, }; use call::{ActiveCall, ParticipantLocation, Room}; use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore}; @@ -17,19 +21,25 @@ use gpui::{ AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::Project; +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, ] @@ -40,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 { @@ -48,6 +60,8 @@ pub struct CollabTitlebarItem { client: Arc, workspace: WeakViewHandle, contacts_popover: Option>, + branch_popover: Option>, + project_popover: Option>, user_menu: ViewHandle, _subscriptions: Vec, } @@ -68,37 +82,43 @@ 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(); if let Some(((user, peer_id), room)) = user + .as_ref() .zip(peer_id) .zip(ActiveCall::global(cx).read(cx).room().cloned()) { - left_container - .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); - - right_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx)); right_container - .add_child(self.render_current_user(&workspace, &theme, &user, peer_id, cx)); + .add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx)); + right_container.add_child(self.render_leave_call(&theme, cx)); + let muted = room.read(cx).is_muted(); + let speaking = room.read(cx).is_speaking(); + left_container.add_child( + self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx), + ); + left_container.add_children(self.render_collaborators(&workspace, &theme, &room, cx)); + right_container.add_child(self.render_toggle_mute(&theme, &room, cx)); + right_container.add_child(self.render_toggle_deafen(&theme, &room, cx)); right_container.add_child(self.render_toggle_screen_sharing_button(&theme, &room, cx)); } let status = workspace.read(cx).client().status(); let status = &*status.borrow(); - if matches!(status, client::Status::Connected { .. }) { right_container.add_child(self.render_toggle_contacts_button(&theme, cx)); - right_container.add_child(self.render_user_menu_button(&theme, cx)); + let avatar = user.as_ref().and_then(|user| user.avatar.clone()); + right_container.add_child(self.render_user_menu_button(&theme, avatar, cx)); } else { right_container.add_children(self.render_connection_status(status, cx)); right_container.add_child(self.render_sign_in_button(&theme, cx)); + right_container.add_child(self.render_user_menu_button(&theme, None, cx)); } Stack::new() @@ -108,7 +128,6 @@ impl View for CollabTitlebarItem { .with_child( right_container.contained().with_background_color( theme - .workspace .titlebar .container .background_color @@ -163,7 +182,6 @@ impl CollabTitlebarItem { }), ); - let view_id = cx.view_id(); Self { workspace: workspace.weak_handle(), project, @@ -171,69 +189,105 @@ impl CollabTitlebarItem { client, contacts_popover: None, user_menu: cx.add_view(|cx| { + let view_id = cx.view_id(); let mut menu = ContextMenu::new(view_id, cx); 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, - cx: &ViewContext, + cx: &mut ViewContext, ) -> AnyElement { - let 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); - fn push_str(buffer: &mut String, index: &mut usize, str: &str) { - buffer.push_str(str); - *index += str.chars().count(); - } + 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()) + }); - let mut indices = Vec::new(); - let mut index = 0; - let mut title = String::new(); - let mut names_and_branches = names_and_branches.peekable(); - while let Some((name, entry)) = names_and_branches.next() { - let pre_index = index; - push_str(&mut title, &mut index, name); - indices.extend((pre_index..index).into_iter()); - if let Some(branch) = entry.and_then(|entry| entry.branch()) { - push_str(&mut title, &mut index, "/"); - push_str(&mut title, &mut index, &branch); - } - if names_and_branches.peek().is_some() { - push_str(&mut title, &mut index, ", "); - if index >= MAX_TITLE_LENGTH { - title.push_str(" …"); - break; - } - } - } - - let text_style = theme.workspace.titlebar.title.clone(); - let item_spacing = theme.workspace.titlebar.item_spacing; - - let mut highlight = text_style.clone(); - highlight.color = theme.workspace.titlebar.highlight_color; - - let style = LabelStyle { - text: text_style, - highlight_text: Some(highlight), + names_and_branches.next().unwrap_or(("", None)) }; - Label::new(title, style) - .with_highlights(indices) - .contained() - .with_margin_right(item_spacing) - .aligned() - .left() - .into_any_named("title-with-git-information") + let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH); + let branch_prepended = entry + .as_ref() + .and_then(RepositoryEntry::branch) + .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 ret = Flex::row().with_child( + Stack::new() + .with_child( + MouseEventHandler::::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( + Flex::row() + .with_child( + Label::new("/", divider_style.text) + .contained() + .with_style(divider_style.container) + .aligned() + .left(), + ) + .with_child( + Stack::new() + .with_child( + MouseEventHandler::::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() } fn window_activation_changed(&mut self, active: bool, cx: &mut ViewContext) { @@ -297,55 +351,167 @@ impl CollabTitlebarItem { } pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext) { - let theme = theme::current(cx).clone(); - let avatar_style = theme.workspace.titlebar.leader_avatar.clone(); - let item_style = theme.context_menu.item.disabled_style().clone(); self.user_menu.update(cx, |user_menu, cx| { - let items = if let Some(user) = self.user_store.read(cx).current_user() { + let items = if let Some(_) = self.user_store.read(cx).current_user() { vec![ - ContextMenuItem::Static(Box::new(move |_| { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Self::render_face( - avatar, - avatar_style.clone(), - Color::transparent_black(), - ) - })) - .with_child(Label::new( - user.github_login.clone(), - item_style.label.clone(), - )) - .contained() - .with_style(item_style.container) - .into_any() - })), - ContextMenuItem::action("Sign out", SignOut), + ContextMenuItem::action("Settings", zed_actions::OpenSettings), + ContextMenuItem::action("Theme", theme_selector::Toggle), + ContextMenuItem::separator(), ContextMenuItem::action( - "Send Feedback", + "Share Feedback", feedback::feedback_editor::GiveFeedback, ), + ContextMenuItem::action("Sign out", SignOut), ] } else { vec![ - ContextMenuItem::action("Sign in", SignIn), + ContextMenuItem::action("Settings", zed_actions::OpenSettings), + ContextMenuItem::action("Theme", theme_selector::Toggle), + ContextMenuItem::separator(), ContextMenuItem::action( - "Send Feedback", + "Share Feedback", feedback::feedback_editor::GiveFeedback, ), ] }; - - user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx); + 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, + ) -> Option> { + self.branch_popover.as_ref().map(|child| { + let theme = theme::current(cx).clone(); + let child = ChildView::new(child, cx); + let child = MouseEventHandler::::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, + ) -> Option> { + self.project_popover.as_ref().map(|child| { + let theme = theme::current(cx).clone(); + let child = ChildView::new(child, cx); + let child = MouseEventHandler::::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) { + 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) { + 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, cx: &mut ViewContext, ) -> AnyElement { - let titlebar = &theme.workspace.titlebar; + let titlebar = &theme.titlebar; let badge = if self .user_store @@ -361,8 +527,20 @@ impl CollabTitlebarItem { .contained() .with_style(titlebar.toggle_contacts_badge) .contained() - .with_margin_left(titlebar.toggle_contacts_button.default.icon_width) - .with_margin_top(titlebar.toggle_contacts_button.default.icon_width) + .with_margin_left( + titlebar + .toggle_contacts_button + .inactive_state() + .default + .icon_width, + ) + .with_margin_top( + titlebar + .toggle_contacts_button + .inactive_state() + .default + .icon_width, + ) .aligned(), ) }; @@ -372,8 +550,9 @@ impl CollabTitlebarItem { MouseEventHandler::::new(0, cx, |state, _| { let style = titlebar .toggle_contacts_button - .style_for(state, self.contacts_popover.is_some()); - Svg::new("icons/user_plus_16.svg") + .in_state(self.contacts_popover.is_some()) + .style_for(state); + Svg::new("icons/radix/person.svg") .with_color(style.color) .constrained() .with_width(style.icon_width) @@ -400,7 +579,6 @@ impl CollabTitlebarItem { .with_children(self.render_contacts_popover_host(titlebar, cx)) .into_any() } - fn render_toggle_screen_sharing_button( &self, theme: &Theme, @@ -410,16 +588,21 @@ impl CollabTitlebarItem { let icon; let tooltip; if room.read(cx).is_screen_sharing() { - icon = "icons/enable_screen_sharing_12.svg"; + icon = "icons/radix/desktop.svg"; tooltip = "Stop Sharing Screen" } else { - icon = "icons/disable_screen_sharing_12.svg"; + icon = "icons/radix/desktop.svg"; tooltip = "Share Screen"; } - let titlebar = &theme.workspace.titlebar; + let active = room.read(cx).is_screen_sharing(); + let titlebar = &theme.titlebar; MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar.call_control.style_for(state, false); + let style = titlebar + .screen_share_button + .in_state(active) + .style_for(state); + Svg::new(icon) .with_color(style.color) .constrained() @@ -445,7 +628,141 @@ impl CollabTitlebarItem { .aligned() .into_any() } + fn render_toggle_mute( + &self, + theme: &Theme, + room: &ModelHandle, + cx: &mut ViewContext, + ) -> AnyElement { + let icon; + let tooltip; + let is_muted = room.read(cx).is_muted(); + if is_muted { + icon = "icons/radix/mic-mute.svg"; + tooltip = "Unmute microphone\nRight click for options"; + } else { + icon = "icons/radix/mic.svg"; + tooltip = "Mute microphone\nRight click for options"; + } + let titlebar = &theme.titlebar; + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar + .toggle_microphone_button + .in_state(is_muted) + .style_for(state); + let image = Svg::new(icon) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container); + if let Some(color) = style.container.background_color { + image.with_background_color(color) + } else { + image + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, _, cx| { + toggle_mute(&Default::default(), cx) + }) + .with_tooltip::( + 0, + tooltip.into(), + Some(Box::new(ToggleMute)), + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any() + } + fn render_toggle_deafen( + &self, + theme: &Theme, + room: &ModelHandle, + cx: &mut ViewContext, + ) -> AnyElement { + let icon; + let tooltip; + let is_deafened = room.read(cx).is_deafened().unwrap_or(false); + if is_deafened { + icon = "icons/radix/speaker-off.svg"; + tooltip = "Unmute speakers\nRight click for options"; + } else { + icon = "icons/radix/speaker-loud.svg"; + tooltip = "Mute speakers\nRight click for options"; + } + + let titlebar = &theme.titlebar; + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar + .toggle_speakers_button + .in_state(is_deafened) + .style_for(state); + Svg::new(icon) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, _, cx| { + toggle_deafen(&Default::default(), cx) + }) + .with_tooltip::( + 0, + tooltip.into(), + Some(Box::new(ToggleDeafen)), + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any() + } + fn render_leave_call(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { + let icon = "icons/radix/exit.svg"; + let tooltip = "Leave call"; + + let titlebar = &theme.titlebar; + MouseEventHandler::::new(0, cx, |state, _| { + let style = titlebar.leave_call_button.style_for(state); + Svg::new(icon) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, _, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + }) + .with_tooltip::( + 0, + tooltip.into(), + Some(Box::new(LeaveCall)), + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any() + } fn render_in_call_share_unshare_button( &self, workspace: &ViewHandle, @@ -458,14 +775,14 @@ impl CollabTitlebarItem { } let is_shared = project.read(cx).is_shared(); - let label = if is_shared { "Unshare" } else { "Share" }; + let label = if is_shared { "Stop Sharing" } else { "Share" }; let tooltip = if is_shared { - "Unshare project from call participants" + "Stop sharing project with call participants" } else { "Share project with call participants" }; - let titlebar = &theme.workspace.titlebar; + let titlebar = &theme.titlebar; enum ShareUnshare {} Some( @@ -473,7 +790,7 @@ impl CollabTitlebarItem { .with_child( MouseEventHandler::::new(0, cx, |state, _| { //TODO: Ensure this button has consistent width for both text variations - let style = titlebar.share_button.style_for(state, false); + let style = titlebar.share_button.inactive_state().style_for(state); Label::new(label, style.text.clone()) .contained() .with_style(style.container) @@ -496,7 +813,7 @@ impl CollabTitlebarItem { ) .aligned() .contained() - .with_margin_left(theme.workspace.titlebar.item_spacing) + .with_margin_left(theme.titlebar.item_spacing) .into_any(), ) } @@ -504,26 +821,56 @@ impl CollabTitlebarItem { fn render_user_menu_button( &self, theme: &Theme, + avatar: Option>, cx: &mut ViewContext, ) -> AnyElement { - let titlebar = &theme.workspace.titlebar; + let tooltip = theme.tooltip.clone(); + let user_menu_button_style = if avatar.is_some() { + &theme.titlebar.user_menu.user_menu_button_online + } else { + &theme.titlebar.user_menu.user_menu_button_offline + }; + let avatar_style = &user_menu_button_style.avatar; Stack::new() .with_child( MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar.call_control.style_for(state, false); - Svg::new("icons/ellipsis_14.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) + let style = user_menu_button_style + .user_menu + .inactive_state() + .style_for(state); + + let mut dropdown = Flex::row().align_children_center(); + + if let Some(avatar_img) = avatar { + dropdown = dropdown.with_child(Self::render_face( + avatar_img, + *avatar_style, + Color::transparent_black(), + None, + )); + }; + + dropdown + .with_child( + Svg::new("icons/caret_down_8.svg") + .with_color(user_menu_button_style.icon.color) + .constrained() + .with_width(user_menu_button_style.icon.width) + .contained() + .into_any(), + ) .aligned() .constrained() - .with_width(style.button_width) - .with_height(style.button_width) + .with_height(style.width) .contained() .with_style(style.container) + .into_any() }) .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, this, cx| { + this.user_menu.update(cx, |menu, _| menu.delay_cancel()); + }) .on_click(MouseButton::Left, move |_, this, cx| { this.toggle_user_menu(&Default::default(), cx) }) @@ -531,11 +878,10 @@ impl CollabTitlebarItem { 0, "Toggle user menu".to_owned(), Some(Box::new(ToggleUserMenu)), - theme.tooltip.clone(), + tooltip, cx, ) - .contained() - .with_margin_left(theme.workspace.titlebar.item_spacing), + .contained(), ) .with_child( ChildView::new(&self.user_menu, cx) @@ -547,9 +893,9 @@ impl CollabTitlebarItem { } fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext) -> AnyElement { - let titlebar = &theme.workspace.titlebar; + let titlebar = &theme.titlebar; MouseEventHandler::::new(0, cx, |state, _| { - let style = titlebar.sign_in_prompt.style_for(state, false); + let style = titlebar.sign_in_button.inactive_state().style_for(state); Label::new("Sign In", style.text.clone()) .contained() .with_style(style.container) @@ -572,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() @@ -611,11 +957,13 @@ impl CollabTitlebarItem { replica_id, participant.peer_id, Some(participant.location), + participant.muted, + participant.speaking, workspace, theme, cx, )) - .with_margin_right(theme.workspace.titlebar.face_pile_spacing), + .with_margin_right(theme.titlebar.face_pile_spacing), ) }) .collect() @@ -627,19 +975,24 @@ impl CollabTitlebarItem { theme: &Theme, user: &Arc, peer_id: PeerId, + muted: bool, + speaking: bool, cx: &mut ViewContext, ) -> AnyElement { let replica_id = workspace.read(cx).project().read(cx).replica_id(); + Container::new(self.render_face_pile( user, Some(replica_id), peer_id, None, + muted, + speaking, workspace, theme, cx, )) - .with_margin_right(theme.workspace.titlebar.item_spacing) + .with_margin_right(theme.titlebar.item_spacing) .into_any() } @@ -649,6 +1002,8 @@ impl CollabTitlebarItem { replica_id: Option, peer_id: PeerId, location: Option, + muted: bool, + speaking: bool, workspace: &ViewHandle, theme: &Theme, cx: &mut ViewContext, @@ -671,15 +1026,23 @@ impl CollabTitlebarItem { }) .unwrap_or(false); - let leader_style = theme.workspace.titlebar.leader_avatar; - let follower_style = theme.workspace.titlebar.follower_avatar; + let leader_style = theme.titlebar.leader_avatar; + let follower_style = theme.titlebar.follower_avatar; + + let microphone_state = if muted { + Some(theme.titlebar.muted) + } else if speaking { + Some(theme.titlebar.speaking) + } else { + None + }; let mut background_color = theme - .workspace .titlebar .container .background_color .unwrap_or_default(); + if let Some(replica_id) = replica_id { if followed_by_self { let selection = theme.editor.replica_selection_style(replica_id).selection; @@ -690,11 +1053,12 @@ impl CollabTitlebarItem { let mut content = Stack::new() .with_children(user.avatar.as_ref().map(|avatar| { - let face_pile = FacePile::new(theme.workspace.titlebar.follower_avatar_overlap) + let face_pile = FacePile::new(theme.titlebar.follower_avatar_overlap) .with_child(Self::render_face( avatar.clone(), Self::location_style(workspace, location, leader_style, cx), background_color, + microphone_state, )) .with_children( (|| { @@ -726,6 +1090,7 @@ impl CollabTitlebarItem { avatar.clone(), follower_style, background_color, + None, )) })) })() @@ -735,7 +1100,7 @@ impl CollabTitlebarItem { let mut container = face_pile .contained() - .with_style(theme.workspace.titlebar.leader_selection); + .with_style(theme.titlebar.leader_selection); if let Some(replica_id) = replica_id { if followed_by_self { @@ -752,8 +1117,8 @@ impl CollabTitlebarItem { Some( AvatarRibbon::new(color) .constrained() - .with_width(theme.workspace.titlebar.avatar_ribbon.width) - .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .with_width(theme.titlebar.avatar_ribbon.width) + .with_height(theme.titlebar.avatar_ribbon.height) .aligned() .bottom(), ) @@ -844,12 +1209,13 @@ impl CollabTitlebarItem { avatar: Arc, avatar_style: AvatarStyle, background_color: Color, + microphone_state: Option, ) -> AnyElement { Image::from_data(avatar) .with_style(avatar_style.image) .aligned() .contained() - .with_background_color(background_color) + .with_background_color(microphone_state.unwrap_or(background_color)) .with_corner_radius(avatar_style.outer_corner_radius) .constrained() .with_width(avatar_style.outer_width) @@ -873,22 +1239,22 @@ impl CollabTitlebarItem { | client::Status::Reconnecting { .. } | client::Status::ReconnectionError { .. } => Some( Svg::new("icons/cloud_slash_12.svg") - .with_color(theme.workspace.titlebar.offline_icon.color) + .with_color(theme.titlebar.offline_icon.color) .constrained() - .with_width(theme.workspace.titlebar.offline_icon.width) + .with_width(theme.titlebar.offline_icon.width) .aligned() .contained() - .with_style(theme.workspace.titlebar.offline_icon.container) + .with_style(theme.titlebar.offline_icon.container) .into_any(), ), client::Status::UpgradeRequired => Some( MouseEventHandler::::new(0, cx, |_, _| { Label::new( "Please update Zed to collaborate", - theme.workspace.titlebar.outdated_warning.text.clone(), + theme.titlebar.outdated_warning.text.clone(), ) .contained() - .with_style(theme.workspace.titlebar.outdated_warning.container) + .with_style(theme.titlebar.outdated_warning.container) .aligned() }) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index c0734388b1..26d9c70a43 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +mod branch_list; mod collab_titlebar_item; mod contact_finder; mod contact_list; @@ -9,15 +10,26 @@ mod notifications; mod project_shared_notification; mod sharing_status_indicator; -use call::ActiveCall; +use call::{ActiveCall, Room}; pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu}; use gpui::{actions, AppContext, Task}; use std::sync::Arc; +use util::ResultExt; use workspace::AppState; -actions!(collab, [ToggleScreenSharing]); +actions!( + collab, + [ + ToggleScreenSharing, + ToggleMute, + ToggleDeafen, + LeaveCall, + ShareMicrophone + ] +); pub fn init(app_state: &Arc, cx: &mut AppContext) { + branch_list::init(cx); collab_titlebar_item::init(cx); contact_list::init(cx); contact_finder::init(cx); @@ -27,6 +39,9 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { sharing_status_indicator::init(cx); cx.add_global_action(toggle_screen_sharing); + cx.add_global_action(toggle_mute); + cx.add_global_action(toggle_deafen); + cx.add_global_action(share_microphone); } pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { @@ -41,3 +56,26 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { toggle_screen_sharing.detach_and_log_err(cx); } } + +pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + room.update(cx, Room::toggle_mute) + .map(|task| task.detach_and_log_err(cx)) + .log_err(); + } +} + +pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + room.update(cx, Room::toggle_deafen) + .map(|task| task.detach_and_log_err(cx)) + .log_err(); + } +} + +pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) { + if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + room.update(cx, Room::share_microphone) + .detach_and_log_err(cx) + } +} diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index b5f2416a5b..af59817ece 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -117,7 +117,8 @@ impl PickerDelegate for ContactFinderDelegate { .contact_finder .picker .item - .style_for(mouse_state, selected); + .in_state(selected) + .style_for(mouse_state); Flex::row() .with_children(user.avatar.clone().map(|avatar| { Image::from_data(avatar) diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index e8dae210c4..428f2156d1 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -514,10 +514,10 @@ impl ContactList { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: participant.user.id, - is_last: projects.peek().is_none() && participant.tracks.is_empty(), + is_last: projects.peek().is_none() && participant.video_tracks.is_empty(), }); } - if !participant.tracks.is_empty() { + if !participant.video_tracks.is_empty() { participant_entries.push(ContactEntry::ParticipantScreen { peer_id: participant.peer_id, is_last: true, @@ -774,7 +774,8 @@ impl ContactList { .with_style( *theme .contact_row - .style_for(&mut Default::default(), is_selected), + .in_state(is_selected) + .style_for(&mut Default::default()), ) .into_any() } @@ -797,7 +798,7 @@ impl ContactList { .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.default; + let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; let line_height = row.name.text.line_height(font_cache); let cap_height = row.name.text.cap_height(font_cache); @@ -810,8 +811,11 @@ impl ContactList { }; MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { - let tree_branch = *tree_branch.style_for(mouse_state, is_selected); - let row = theme.project_row.style_for(mouse_state, is_selected); + let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + let row = theme + .project_row + .in_state(is_selected) + .style_for(mouse_state); Flex::row() .with_child( @@ -893,7 +897,7 @@ impl ContactList { .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.default; + let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; let line_height = row.name.text.line_height(font_cache); let cap_height = row.name.text.cap_height(font_cache); @@ -904,8 +908,11 @@ impl ContactList { peer_id.as_u64() as usize, cx, |mouse_state, _| { - let tree_branch = *tree_branch.style_for(mouse_state, is_selected); - let row = theme.project_row.style_for(mouse_state, is_selected); + let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + let row = theme + .project_row + .in_state(is_selected) + .style_for(mouse_state); Flex::row() .with_child( @@ -989,7 +996,8 @@ impl ContactList { let header_style = theme .header_row - .style_for(&mut Default::default(), is_selected); + .in_state(is_selected) + .style_for(&mut Default::default()); let text = match section { Section::ActiveCall => "Collaborators", Section::Requests => "Contact Requests", @@ -999,7 +1007,7 @@ impl ContactList { let leave_call = if section == Section::ActiveCall { Some( MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.leave_call.style_for(state, false); + let style = theme.leave_call.style_for(state); Label::new("Leave Call", style.text.clone()) .contained() .with_style(style.container) @@ -1110,8 +1118,7 @@ impl ContactList { contact.user.id as usize, cx, |mouse_state, _| { - let button_style = - theme.contact_button.style_for(mouse_state, false); + let button_style = theme.contact_button.style_for(mouse_state); render_icon_button(button_style, "icons/x_mark_8.svg") .aligned() .flex_float() @@ -1146,7 +1153,8 @@ impl ContactList { .with_style( *theme .contact_row - .style_for(&mut Default::default(), is_selected), + .in_state(is_selected) + .style_for(&mut Default::default()), ) }) .on_click(MouseButton::Left, move |_, this, cx| { @@ -1204,7 +1212,7 @@ impl ContactList { let button_style = if is_contact_request_pending { &theme.disabled_button } else { - theme.contact_button.style_for(mouse_state, false) + theme.contact_button.style_for(mouse_state) }; render_icon_button(button_style, "icons/x_mark_8.svg").aligned() }) @@ -1227,7 +1235,7 @@ impl ContactList { let button_style = if is_contact_request_pending { &theme.disabled_button } else { - theme.contact_button.style_for(mouse_state, false) + theme.contact_button.style_for(mouse_state) }; render_icon_button(button_style, "icons/check_8.svg") .aligned() @@ -1250,7 +1258,7 @@ impl ContactList { let button_style = if is_contact_request_pending { &theme.disabled_button } else { - theme.contact_button.style_for(mouse_state, false) + theme.contact_button.style_for(mouse_state) }; render_icon_button(button_style, "icons/x_mark_8.svg") .aligned() @@ -1277,7 +1285,8 @@ impl ContactList { .with_style( *theme .contact_row - .style_for(&mut Default::default(), is_selected), + .in_state(is_selected) + .style_for(&mut Default::default()), ) .into_any() } diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index abeb65b1dc..cbd072fe89 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -53,7 +53,7 @@ where ) .with_child( MouseEventHandler::::new(user.id as usize, cx, |state, _| { - let style = theme.dismiss_button.style_for(state, false); + let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) .constrained() @@ -93,7 +93,7 @@ where .with_children(buttons.into_iter().enumerate().map( |(ix, (message, handler))| { MouseEventHandler::::new(ix, cx, |state, _| { - let button = theme.button.style_for(state, false); + let button = theme.button.style_for(state); Label::new(message, button.text.clone()) .contained() .with_style(button.container) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 2ee93a0734..aec876bd78 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -185,8 +185,8 @@ impl PickerDelegate for CommandPaletteDelegate { let mat = &self.matches[ix]; let command = &self.actions[mat.candidate_id]; let theme = theme::current(cx); - let style = theme.picker.item.style_for(mouse_state, selected); - let key_style = &theme.command_palette.key.style_for(mouse_state, selected); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); + let key_style = &theme.command_palette.key.in_state(selected); let keystroke_spacing = theme.command_palette.keystroke_spacing; Flex::row() diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index fb455fe1d0..f58afab361 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -124,6 +124,7 @@ pub struct ContextMenu { items: Vec, selected_index: Option, visible: bool, + delay_cancel: bool, previously_focused_view_id: Option, parent_view_id: usize, _actions_observation: Subscription, @@ -178,6 +179,7 @@ impl ContextMenu { pub fn new(parent_view_id: usize, cx: &mut ViewContext) -> Self { Self { show_count: 0, + delay_cancel: false, anchor_position: Default::default(), anchor_corner: AnchorCorner::TopLeft, position_mode: OverlayPositionMode::Window, @@ -232,15 +234,22 @@ impl ContextMenu { } } + pub fn delay_cancel(&mut self) { + self.delay_cancel = true; + } + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - self.reset(cx); - 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()); - } - }); + if !self.delay_cancel { + self.reset(cx); + let show_count = self.show_count; + cx.defer(move |this, cx| { + if cx.handle().is_focused(cx) && this.show_count == show_count { + (**cx).focus(this.previously_focused_view_id.take()); + } + }); + } else { + self.delay_cancel = false; + } } fn reset(&mut self, cx: &mut ViewContext) { @@ -293,6 +302,34 @@ impl ContextMenu { } } + pub fn toggle( + &mut self, + anchor_position: Vector2F, + anchor_corner: AnchorCorner, + items: Vec, + cx: &mut ViewContext, + ) { + if self.visible() { + self.cancel(&Cancel, cx); + } else { + let mut items = items.into_iter().peekable(); + if items.peek().is_some() { + self.items = items.collect(); + self.anchor_position = anchor_position; + self.anchor_corner = anchor_corner; + self.visible = true; + self.show_count += 1; + if !cx.is_self_focused() { + self.previously_focused_view_id = cx.focused_view_id(); + } + cx.focus_self(); + } else { + self.visible = false; + } + } + cx.notify(); + } + pub fn show( &mut self, anchor_position: Vector2F, @@ -328,10 +365,8 @@ impl ContextMenu { Flex::column().with_children(self.items.iter().enumerate().map(|(ix, item)| { match item { ContextMenuItem::Item { label, .. } => { - let style = style.item.style_for( - &mut Default::default(), - Some(ix) == self.selected_index, - ); + let style = style.item.in_state(self.selected_index == Some(ix)); + let style = style.style_for(&mut Default::default()); match label { ContextMenuItemLabel::String(label) => { @@ -363,10 +398,8 @@ impl ContextMenu { .with_children(self.items.iter().enumerate().map(|(ix, item)| { match item { ContextMenuItem::Item { action, .. } => { - let style = style.item.style_for( - &mut Default::default(), - Some(ix) == self.selected_index, - ); + let style = style.item.in_state(self.selected_index == Some(ix)); + let style = style.style_for(&mut Default::default()); match action { ContextMenuItemAction::Action(action) => KeystrokeLabel::new( @@ -412,8 +445,8 @@ impl ContextMenu { let action = action.clone(); let view_id = self.parent_view_id; MouseEventHandler::::new(ix, cx, |state, _| { - let style = - style.item.style_for(state, Some(ix) == self.selected_index); + let style = style.item.in_state(self.selected_index == Some(ix)); + let style = style.style_for(state); let keystroke = match &action { ContextMenuItemAction::Action(action) => Some( KeystrokeLabel::new( diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index e73424f0cd..ce4938ed0d 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -15,7 +15,7 @@ use language::{ ToPointUtf16, }; use log::{debug, error}; -use lsp::{LanguageServer, LanguageServerId}; +use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId}; use node_runtime::NodeRuntime; use request::{LogMessage, StatusNotification}; use settings::SettingsStore; @@ -340,7 +340,7 @@ impl Copilot { let http = util::http::FakeHttpClient::create(|_| async { unreachable!() }); let this = cx.add_model(|cx| Self { http: http.clone(), - node_runtime: NodeRuntime::new(http, cx.background().clone()), + node_runtime: NodeRuntime::instance(http, cx.background().clone()), server: CopilotServer::Running(RunningCopilotServer { lsp: Arc::new(server), sign_in_status: SignInStatus::Authorized, @@ -361,11 +361,14 @@ impl Copilot { let start_language_server = async { let server_path = get_copilot_lsp(http).await?; let node_path = node_runtime.binary_path().await?; - let arguments: &[OsString] = &[server_path.into(), "--stdio".into()]; + let arguments: Vec = vec![server_path.into(), "--stdio".into()]; + let binary = LanguageServerBinary { + path: node_path, + arguments, + }; let server = LanguageServer::new( LanguageServerId(0), - &node_path, - arguments, + binary, Path::new("/"), None, cx.clone(), diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 0993a33e6c..803cb5cc85 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -127,16 +127,16 @@ impl CopilotCodeVerification { .with_child( Label::new( if copied { "Copied!" } else { "Copy" }, - device_code_style.cta.style_for(state, false).text.clone(), + device_code_style.cta.style_for(state).text.clone(), ) .aligned() .contained() - .with_style(*device_code_style.right_container.style_for(state, false)) + .with_style(*device_code_style.right_container.style_for(state)) .constrained() .with_width(device_code_style.right), ) .contained() - .with_style(device_code_style.cta.style_for(state, false).container) + .with_style(device_code_style.cta.style_for(state).container) }) .on_click(gpui::platform::MouseButton::Left, { let user_code = data.user_code.clone(); diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 2454074d45..5576451b1b 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -71,7 +71,8 @@ impl View for CopilotButton { .status_bar .panel_buttons .button - .style_for(state, active); + .in_state(active) + .style_for(state); Flex::row() .with_child( @@ -101,6 +102,9 @@ impl View for CopilotButton { } }) .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, |_, this, cx| { + this.popup_menu.update(cx, |menu, _| menu.delay_cancel()); + }) .on_click(MouseButton::Left, { let status = status.clone(); move |_, this, cx| match status { @@ -185,7 +189,7 @@ impl CopilotButton { })); self.popup_menu.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::BottomRight, menu_options, @@ -255,7 +259,7 @@ impl CopilotButton { move |state: &mut MouseState, style: &theme::ContextMenuItem| { Flex::row() .with_child(Label::new("Copilot Settings", style.label.clone())) - .with_child(theme::ui::icon(icon_style.style_for(state, false))) + .with_child(theme::ui::icon(icon_style.style_for(state))) .align_children_center() .into_any() }, @@ -265,7 +269,7 @@ impl CopilotButton { menu_options.push(ContextMenuItem::action("Sign Out", SignOut)); self.popup_menu.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::BottomRight, menu_options, diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 5350e53d6a..d0cd437946 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1509,7 +1509,8 @@ mod tests { let snapshot = editor.snapshot(cx); snapshot .blocks_in_range(0..snapshot.max_point().row()) - .filter_map(|(row, block)| { + .enumerate() + .filter_map(|(ix, (row, block))| { let name = match block { TransformBlock::Custom(block) => block .render(&mut BlockContext { @@ -1520,6 +1521,7 @@ mod tests { gutter_width: 0., line_height: 0., em_width: 0., + block_id: ix, }) .name()? .to_string(), diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index f84846eae1..c106f042b5 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -100,7 +100,7 @@ impl View for DiagnosticIndicator { .workspace .status_bar .diagnostic_summary - .style_for(state, false); + .style_for(state); let mut summary_row = Flex::row(); if self.summary.error_count > 0 { @@ -198,7 +198,7 @@ impl View for DiagnosticIndicator { MouseEventHandler::::new(1, cx, |state, _| { Label::new( diagnostic.message.split('\n').next().unwrap().to_string(), - message_style.style_for(state, false).text.clone(), + message_style.style_for(state).text.clone(), ) .aligned() .contained() diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index a594af51a6..6e04833f17 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1,24 +1,23 @@ mod block_map; mod fold_map; -mod suggestion_map; +mod inlay_map; mod tab_map; mod wrap_map; -use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; -use fold_map::{FoldMap, FoldOffset}; +use fold_map::FoldMap; use gpui::{ color::Color, fonts::{FontId, HighlightStyle}, Entity, ModelContext, ModelHandle, }; +use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, }; use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; -pub use suggestion_map::Suggestion; -use suggestion_map::SuggestionMap; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; use wrap_map::WrapMap; @@ -28,6 +27,8 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; +pub use self::inlay_map::Inlay; + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { Folded, @@ -44,7 +45,7 @@ pub struct DisplayMap { buffer: ModelHandle, buffer_subscription: BufferSubscription, fold_map: FoldMap, - suggestion_map: SuggestionMap, + inlay_map: InlayMap, tab_map: TabMap, wrap_map: ModelHandle, block_map: BlockMap, @@ -69,8 +70,8 @@ impl DisplayMap { let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let tab_size = Self::tab_size(&buffer, cx); - let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx)); - let (suggestion_map, snapshot) = SuggestionMap::new(snapshot); + let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + let (fold_map, snapshot) = FoldMap::new(snapshot); let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx); let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height); @@ -79,7 +80,7 @@ impl DisplayMap { buffer, buffer_subscription, fold_map, - suggestion_map, + inlay_map, tab_map, wrap_map, block_map, @@ -88,16 +89,13 @@ impl DisplayMap { } } - pub fn snapshot(&self, cx: &mut ModelContext) -> DisplaySnapshot { + pub fn snapshot(&mut self, cx: &mut ModelContext) -> DisplaySnapshot { let buffer_snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); - let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits); - let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits); - + let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits); + let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits); let tab_size = Self::tab_size(&self.buffer, cx); - let (tab_snapshot, edits) = self - .tab_map - .sync(suggestion_snapshot.clone(), edits, tab_size); + let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size); let (wrap_snapshot, edits) = self .wrap_map .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx)); @@ -106,7 +104,7 @@ impl DisplayMap { DisplaySnapshot { buffer_snapshot: self.buffer.read(cx).snapshot(cx), fold_snapshot, - suggestion_snapshot, + inlay_snapshot, tab_snapshot, wrap_snapshot, block_snapshot, @@ -132,15 +130,14 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); - let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); self.block_map.read(snapshot, edits); let (snapshot, edits) = fold_map.fold(ranges); - let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self .wrap_map @@ -157,15 +154,14 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits); - let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); self.block_map.read(snapshot, edits); let (snapshot, edits) = fold_map.unfold(ranges, inclusive); - let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self .wrap_map @@ -181,8 +177,8 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); - let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self .wrap_map @@ -199,8 +195,8 @@ impl DisplayMap { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.inlay_map.sync(snapshot, edits); let (snapshot, edits) = self.fold_map.read(snapshot, edits); - let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits); let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); let (snapshot, edits) = self .wrap_map @@ -231,32 +227,6 @@ impl DisplayMap { self.text_highlights.remove(&Some(type_id)) } - pub fn has_suggestion(&self) -> bool { - self.suggestion_map.has_suggestion() - } - - pub fn replace_suggestion( - &self, - new_suggestion: Option>, - cx: &mut ModelContext, - ) -> Option> - where - T: ToPoint, - { - let snapshot = self.buffer.read(cx).snapshot(cx); - let edits = self.buffer_subscription.consume().into_inner(); - let tab_size = Self::tab_size(&self.buffer, cx); - let (snapshot, edits) = self.fold_map.read(snapshot, edits); - let (snapshot, edits, old_suggestion) = - self.suggestion_map.replace(new_suggestion, snapshot, edits); - let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); - let (snapshot, edits) = self - .wrap_map - .update(cx, |map, cx| map.sync(snapshot, edits, cx)); - self.block_map.read(snapshot, edits); - old_suggestion - } - pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -271,6 +241,39 @@ impl DisplayMap { .update(cx, |map, cx| map.set_wrap_width(width, cx)) } + pub fn current_inlays(&self) -> impl Iterator { + self.inlay_map.current_inlays() + } + + pub fn splice_inlays( + &mut self, + to_remove: Vec, + to_insert: Vec, + cx: &mut ModelContext, + ) { + if to_remove.is_empty() && to_insert.is_empty() { + return; + } + let buffer_snapshot = self.buffer.read(cx).snapshot(cx); + let edits = self.buffer_subscription.consume().into_inner(); + let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits); + let (snapshot, edits) = self.fold_map.read(snapshot, edits); + let tab_size = Self::tab_size(&self.buffer, cx); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + + let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert); + let (snapshot, edits) = self.fold_map.read(snapshot, edits); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits); + } + fn tab_size(buffer: &ModelHandle, cx: &mut ModelContext) -> NonZeroU32 { let language = buffer .read(cx) @@ -288,7 +291,7 @@ impl DisplayMap { pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, fold_snapshot: fold_map::FoldSnapshot, - suggestion_snapshot: suggestion_map::SuggestionSnapshot, + inlay_snapshot: inlay_map::InlaySnapshot, tab_snapshot: tab_map::TabSnapshot, wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, @@ -316,9 +319,11 @@ impl DisplaySnapshot { pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { loop { - let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left); - *fold_point.column_mut() = 0; - point = fold_point.to_buffer_point(&self.fold_snapshot); + let mut inlay_point = self.inlay_snapshot.to_inlay_point(point); + let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left); + fold_point.0.column = 0; + inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); + point = self.inlay_snapshot.to_buffer_point(inlay_point); let mut display_point = self.point_to_display_point(point, Bias::Left); *display_point.column_mut() = 0; @@ -332,9 +337,11 @@ impl DisplaySnapshot { pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { loop { - let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right); - *fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row()); - point = fold_point.to_buffer_point(&self.fold_snapshot); + let mut inlay_point = self.inlay_snapshot.to_inlay_point(point); + let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right); + fold_point.0.column = self.fold_snapshot.line_len(fold_point.row()); + inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); + point = self.inlay_snapshot.to_buffer_point(inlay_point); let mut display_point = self.point_to_display_point(point, Bias::Right); *display_point.column_mut() = self.line_len(display_point.row()); @@ -364,9 +371,9 @@ impl DisplaySnapshot { } fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { - let fold_point = self.fold_snapshot.to_fold_point(point, bias); - let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point); - let tab_point = self.tab_snapshot.to_tab_point(suggestion_point); + let inlay_point = self.inlay_snapshot.to_inlay_point(point); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let tab_point = self.tab_snapshot.to_tab_point(fold_point); let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); let block_point = self.block_snapshot.to_block_point(wrap_point); DisplayPoint(block_point) @@ -376,9 +383,9 @@ impl DisplaySnapshot { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); - let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0; - let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point); - fold_point.to_buffer_point(&self.fold_snapshot) + let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; + let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); + self.inlay_snapshot.to_buffer_point(inlay_point) } pub fn max_point(&self) -> DisplayPoint { @@ -388,7 +395,13 @@ impl DisplaySnapshot { /// Returns text chunks starting at the given display row until the end of the file pub fn text_chunks(&self, display_row: u32) -> impl Iterator { self.block_snapshot - .chunks(display_row..self.max_point().row() + 1, false, None, None) + .chunks( + display_row..self.max_point().row() + 1, + false, + None, + None, + None, + ) .map(|h| h.text) } @@ -396,7 +409,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None) + .chunks(row..row + 1, false, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -408,13 +421,15 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - suggestion_highlight: Option, + hint_highlights: Option, + suggestion_highlights: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - suggestion_highlight, + hint_highlights, + suggestion_highlights, ) } @@ -790,9 +805,10 @@ impl DisplayPoint { pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize { let wrap_point = map.block_snapshot.to_wrap_point(self.0); let tab_point = map.wrap_snapshot.to_tab_point(wrap_point); - let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0; - let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point); - fold_point.to_buffer_offset(&map.fold_snapshot) + let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0; + let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot); + map.inlay_snapshot + .to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point)) } } @@ -1706,7 +1722,7 @@ pub mod tests { ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option, Option)> = Vec::new(); - for chunk in snapshot.chunks(rows, true, None) { + for chunk in snapshot.chunks(rows, true, None, None) { let syntax_color = chunk .syntax_highlight_id .and_then(|id| id.style(theme)?.color); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 05ff9886f1..4b76ded3d5 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -88,6 +88,7 @@ pub struct BlockContext<'a, 'b, 'c> { pub gutter_padding: f32, pub em_width: f32, pub line_height: f32, + pub block_id: usize, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -243,7 +244,7 @@ impl BlockMap { // Preserve any old transforms that precede this edit. let old_start = WrapRow(edit.old.start); let new_start = WrapRow(edit.new.start); - new_transforms.push_tree(cursor.slice(&old_start, Bias::Left, &()), &()); + new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &()); if let Some(transform) = cursor.item() { if transform.is_isomorphic() && old_start == cursor.end(&()) { new_transforms.push(transform.clone(), &()); @@ -425,7 +426,7 @@ impl BlockMap { push_isomorphic(&mut new_transforms, extent_after_edit); } - new_transforms.push_tree(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(&()), &()); debug_assert_eq!( new_transforms.summary().input_rows, wrap_snapshot.max_point().row() + 1 @@ -572,9 +573,15 @@ impl<'a> BlockMapWriter<'a> { impl BlockSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(0..self.transforms.summary().output_rows, false, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + 0..self.transforms.summary().output_rows, + false, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } pub fn chunks<'a>( @@ -582,7 +589,8 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - suggestion_highlight: Option, + hint_highlights: Option, + suggestion_highlights: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -615,7 +623,8 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - suggestion_highlight, + hint_highlights, + suggestion_highlights, ), input_chunk: Default::default(), transforms: cursor, @@ -988,7 +997,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) { #[cfg(test)] mod tests { use super::*; - use crate::display_map::suggestion_map::SuggestionMap; + use crate::display_map::inlay_map::InlayMap; use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; use crate::multi_buffer::MultiBuffer; use gpui::{elements::Empty, Element}; @@ -1029,9 +1038,9 @@ mod tests { let buffer = MultiBuffer::build_simple(text, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); - let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap()); let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx); let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); @@ -1174,12 +1183,11 @@ mod tests { buffer.snapshot(cx) }); - let (fold_snapshot, fold_edits) = - fold_map.read(buffer_snapshot, subscription.consume().into_inner()); - let (suggestion_snapshot, suggestion_edits) = - suggestion_map.sync(fold_snapshot, fold_edits); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = - tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap()); + tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap()); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); @@ -1204,9 +1212,9 @@ mod tests { let buffer = MultiBuffer::build_simple(text, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx); let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1); @@ -1276,9 +1284,9 @@ mod tests { }; let mut buffer_snapshot = buffer.read(cx).snapshot(cx); - let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx); let mut block_map = BlockMap::new( @@ -1331,12 +1339,11 @@ mod tests { }) .collect::>(); - let (fold_snapshot, fold_edits) = - fold_map.read(buffer_snapshot.clone(), vec![]); - let (suggestion_snapshot, suggestion_edits) = - suggestion_map.sync(fold_snapshot, fold_edits); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = - tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size); + tab_map.sync(fold_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); @@ -1356,12 +1363,11 @@ mod tests { }) .collect(); - let (fold_snapshot, fold_edits) = - fold_map.read(buffer_snapshot.clone(), vec![]); - let (suggestion_snapshot, suggestion_edits) = - suggestion_map.sync(fold_snapshot, fold_edits); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), vec![]); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tab_snapshot, tab_edits) = - tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size); + tab_map.sync(fold_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); @@ -1380,11 +1386,10 @@ mod tests { } } - let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits); - let (suggestion_snapshot, suggestion_edits) = - suggestion_map.sync(fold_snapshot, fold_edits); - let (tab_snapshot, tab_edits) = - tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); + let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { wrap_map.sync(tab_snapshot, tab_edits, cx) }); @@ -1498,6 +1503,7 @@ mod tests { false, None, None, + None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 6ef1ebce1d..0b1523fe75 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,19 +1,15 @@ -use super::TextHighlights; -use crate::{ - multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot, - ToOffset, +use super::{ + inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, + TextHighlights, }; -use collections::BTreeMap; +use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; use language::{Chunk, Edit, Point, TextSummary}; -use parking_lot::Mutex; use std::{ any::TypeId, cmp::{self, Ordering}, - iter::{self, Peekable}, - ops::{Range, Sub}, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, - vec, + iter, + ops::{Add, AddAssign, Range, Sub}, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; @@ -29,28 +25,24 @@ impl FoldPoint { self.0.row } + pub fn column(self) -> u32 { + self.0.column + } + pub fn row_mut(&mut self) -> &mut u32 { &mut self.0.row } + #[cfg(test)] pub fn column_mut(&mut self) -> &mut u32 { &mut self.0.column } - pub fn to_buffer_point(self, snapshot: &FoldSnapshot) -> Point { - let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>(); + pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { + let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(); cursor.seek(&self, Bias::Right, &()); let overshoot = self.0 - cursor.start().0 .0; - cursor.start().1 + overshoot - } - - pub fn to_buffer_offset(self, snapshot: &FoldSnapshot) -> usize { - let mut cursor = snapshot.transforms.cursor::<(FoldPoint, Point)>(); - cursor.seek(&self, Bias::Right, &()); - let overshoot = self.0 - cursor.start().0 .0; - snapshot - .buffer_snapshot - .point_to_offset(cursor.start().1 + overshoot) + InlayPoint(cursor.start().1 .0 + overshoot) } pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset { @@ -63,10 +55,10 @@ impl FoldPoint { if !overshoot.is_zero() { let transform = cursor.item().expect("display point out of range"); assert!(transform.output_text.is_none()); - let end_buffer_offset = snapshot - .buffer_snapshot - .point_to_offset(cursor.start().1.input.lines + overshoot); - offset += end_buffer_offset - cursor.start().1.input.len; + let end_inlay_offset = snapshot + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot)); + offset += end_inlay_offset.0 - cursor.start().1.input.len; } FoldOffset(offset) } @@ -87,8 +79,9 @@ impl<'a> FoldMapWriter<'a> { ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let mut folds = Vec::new(); - let buffer = self.0.buffer.lock().clone(); + let snapshot = self.0.snapshot.inlay_snapshot.clone(); for range in ranges.into_iter() { + let buffer = &snapshot.buffer; let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer); // Ignore any empty ranges. @@ -103,35 +96,32 @@ impl<'a> FoldMapWriter<'a> { } folds.push(fold); - edits.push(text::Edit { - old: range.clone(), - new: range, + + let inlay_range = + snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range, }); } - folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, &buffer)); + let buffer = &snapshot.buffer; + folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer)); - self.0.folds = { + self.0.snapshot.folds = { let mut new_tree = SumTree::new(); - let mut cursor = self.0.folds.cursor::(); + let mut cursor = self.0.snapshot.folds.cursor::(); for fold in folds { - new_tree.push_tree(cursor.slice(&fold, Bias::Right, &buffer), &buffer); - new_tree.push(fold, &buffer); + new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer); + new_tree.push(fold, buffer); } - new_tree.push_tree(cursor.suffix(&buffer), &buffer); + new_tree.append(cursor.suffix(buffer), buffer); new_tree }; - consolidate_buffer_edits(&mut edits); - let edits = self.0.sync(buffer.clone(), edits); - let snapshot = FoldSnapshot { - transforms: self.0.transforms.lock().clone(), - folds: self.0.folds.clone(), - buffer_snapshot: buffer, - version: self.0.version.load(SeqCst), - ellipses_color: self.0.ellipses_color, - }; - (snapshot, edits) + consolidate_inlay_edits(&mut edits); + let edits = self.0.sync(snapshot.clone(), edits); + (self.0.snapshot.clone(), edits) } pub fn unfold( @@ -141,110 +131,93 @@ impl<'a> FoldMapWriter<'a> { ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let mut fold_ixs_to_delete = Vec::new(); - let buffer = self.0.buffer.lock().clone(); + let snapshot = self.0.snapshot.inlay_snapshot.clone(); + let buffer = &snapshot.buffer; for range in ranges.into_iter() { // Remove intersecting folds and add their ranges to edits that are passed to sync. - let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive); + let mut folds_cursor = + intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive); while let Some(fold) = folds_cursor.item() { - let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer); + let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer); if offset_range.end > offset_range.start { - edits.push(text::Edit { - old: offset_range.clone(), - new: offset_range, + let inlay_range = snapshot.to_inlay_offset(offset_range.start) + ..snapshot.to_inlay_offset(offset_range.end); + edits.push(InlayEdit { + old: inlay_range.clone(), + new: inlay_range, }); } fold_ixs_to_delete.push(*folds_cursor.start()); - folds_cursor.next(&buffer); + folds_cursor.next(buffer); } } fold_ixs_to_delete.sort_unstable(); fold_ixs_to_delete.dedup(); - self.0.folds = { - let mut cursor = self.0.folds.cursor::(); + self.0.snapshot.folds = { + let mut cursor = self.0.snapshot.folds.cursor::(); let mut folds = SumTree::new(); for fold_ix in fold_ixs_to_delete { - folds.push_tree(cursor.slice(&fold_ix, Bias::Right, &buffer), &buffer); - cursor.next(&buffer); + folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer); + cursor.next(buffer); } - folds.push_tree(cursor.suffix(&buffer), &buffer); + folds.append(cursor.suffix(buffer), buffer); folds }; - consolidate_buffer_edits(&mut edits); - let edits = self.0.sync(buffer.clone(), edits); - let snapshot = FoldSnapshot { - transforms: self.0.transforms.lock().clone(), - folds: self.0.folds.clone(), - buffer_snapshot: buffer, - version: self.0.version.load(SeqCst), - ellipses_color: self.0.ellipses_color, - }; - (snapshot, edits) + consolidate_inlay_edits(&mut edits); + let edits = self.0.sync(snapshot.clone(), edits); + (self.0.snapshot.clone(), edits) } } pub struct FoldMap { - buffer: Mutex, - transforms: Mutex>, - folds: SumTree, - version: AtomicUsize, + snapshot: FoldSnapshot, ellipses_color: Option, } impl FoldMap { - pub fn new(buffer: MultiBufferSnapshot) -> (Self, FoldSnapshot) { + pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) { let this = Self { - buffer: Mutex::new(buffer.clone()), - folds: Default::default(), - transforms: Mutex::new(SumTree::from_item( - Transform { - summary: TransformSummary { - input: buffer.text_summary(), - output: buffer.text_summary(), + snapshot: FoldSnapshot { + folds: Default::default(), + transforms: SumTree::from_item( + Transform { + summary: TransformSummary { + input: inlay_snapshot.text_summary(), + output: inlay_snapshot.text_summary(), + }, + output_text: None, }, - output_text: None, - }, - &(), - )), - ellipses_color: None, - version: Default::default(), - }; - - let snapshot = FoldSnapshot { - transforms: this.transforms.lock().clone(), - folds: this.folds.clone(), - buffer_snapshot: this.buffer.lock().clone(), - version: this.version.load(SeqCst), + &(), + ), + inlay_snapshot: inlay_snapshot.clone(), + version: 0, + ellipses_color: None, + }, ellipses_color: None, }; + let snapshot = this.snapshot.clone(); (this, snapshot) } pub fn read( - &self, - buffer: MultiBufferSnapshot, - edits: Vec>, + &mut self, + inlay_snapshot: InlaySnapshot, + edits: Vec, ) -> (FoldSnapshot, Vec) { - let edits = self.sync(buffer, edits); + let edits = self.sync(inlay_snapshot, edits); self.check_invariants(); - let snapshot = FoldSnapshot { - transforms: self.transforms.lock().clone(), - folds: self.folds.clone(), - buffer_snapshot: self.buffer.lock().clone(), - version: self.version.load(SeqCst), - ellipses_color: self.ellipses_color, - }; - (snapshot, edits) + (self.snapshot.clone(), edits) } pub fn write( &mut self, - buffer: MultiBufferSnapshot, - edits: Vec>, + inlay_snapshot: InlaySnapshot, + edits: Vec, ) -> (FoldMapWriter, FoldSnapshot, Vec) { - let (snapshot, edits) = self.read(buffer, edits); + let (snapshot, edits) = self.read(inlay_snapshot, edits); (FoldMapWriter(self), snapshot, edits) } @@ -260,15 +233,17 @@ impl FoldMap { fn check_invariants(&self) { if cfg!(test) { assert_eq!( - self.transforms.lock().summary().input.len, - self.buffer.lock().len(), - "transform tree does not match buffer's length" + self.snapshot.transforms.summary().input.len, + self.snapshot.inlay_snapshot.len().0, + "transform tree does not match inlay snapshot's length" ); - let mut folds = self.folds.iter().peekable(); + let mut folds = self.snapshot.folds.iter().peekable(); while let Some(fold) = folds.next() { if let Some(next_fold) = folds.peek() { - let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()); + let comparison = fold + .0 + .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer); assert!(comparison.is_le()); } } @@ -276,50 +251,42 @@ impl FoldMap { } fn sync( - &self, - new_buffer: MultiBufferSnapshot, - buffer_edits: Vec>, + &mut self, + inlay_snapshot: InlaySnapshot, + inlay_edits: Vec, ) -> Vec { - if buffer_edits.is_empty() { - let mut buffer = self.buffer.lock(); - if buffer.edit_count() != new_buffer.edit_count() - || buffer.parse_count() != new_buffer.parse_count() - || buffer.diagnostics_update_count() != new_buffer.diagnostics_update_count() - || buffer.git_diff_update_count() != new_buffer.git_diff_update_count() - || buffer.trailing_excerpt_update_count() - != new_buffer.trailing_excerpt_update_count() - { - self.version.fetch_add(1, SeqCst); + if inlay_edits.is_empty() { + if self.snapshot.inlay_snapshot.version != inlay_snapshot.version { + self.snapshot.version += 1; } - *buffer = new_buffer; + self.snapshot.inlay_snapshot = inlay_snapshot; Vec::new() } else { - let mut buffer_edits_iter = buffer_edits.iter().cloned().peekable(); + let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable(); let mut new_transforms = SumTree::new(); - let mut transforms = self.transforms.lock(); - let mut cursor = transforms.cursor::(); - cursor.seek(&0, Bias::Right, &()); + let mut cursor = self.snapshot.transforms.cursor::(); + cursor.seek(&InlayOffset(0), Bias::Right, &()); - while let Some(mut edit) = buffer_edits_iter.next() { - new_transforms.push_tree(cursor.slice(&edit.old.start, Bias::Left, &()), &()); - edit.new.start -= edit.old.start - cursor.start(); + while let Some(mut edit) = inlay_edits_iter.next() { + new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &()); + edit.new.start -= edit.old.start - *cursor.start(); edit.old.start = *cursor.start(); cursor.seek(&edit.old.end, Bias::Right, &()); cursor.next(&()); - let mut delta = edit.new.len() as isize - edit.old.len() as isize; + let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize; loop { edit.old.end = *cursor.start(); - if let Some(next_edit) = buffer_edits_iter.peek() { + if let Some(next_edit) = inlay_edits_iter.peek() { if next_edit.old.start > edit.old.end { break; } - let next_edit = buffer_edits_iter.next().unwrap(); - delta += next_edit.new.len() as isize - next_edit.old.len() as isize; + let next_edit = inlay_edits_iter.next().unwrap(); + delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize; if next_edit.old.end >= edit.old.end { edit.old.end = next_edit.old.end; @@ -331,19 +298,29 @@ impl FoldMap { } } - edit.new.end = ((edit.new.start + edit.old.len()) as isize + delta) as usize; + edit.new.end = + InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize); - let anchor = new_buffer.anchor_before(edit.new.start); - let mut folds_cursor = self.folds.cursor::(); - folds_cursor.seek(&Fold(anchor..Anchor::max()), Bias::Left, &new_buffer); + let anchor = inlay_snapshot + .buffer + .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start)); + let mut folds_cursor = self.snapshot.folds.cursor::(); + folds_cursor.seek( + &Fold(anchor..Anchor::max()), + Bias::Left, + &inlay_snapshot.buffer, + ); let mut folds = iter::from_fn({ - let buffer = &new_buffer; + let inlay_snapshot = &inlay_snapshot; move || { - let item = folds_cursor - .item() - .map(|f| f.0.start.to_offset(buffer)..f.0.end.to_offset(buffer)); - folds_cursor.next(buffer); + let item = folds_cursor.item().map(|f| { + let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer); + let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer); + inlay_snapshot.to_inlay_offset(buffer_start) + ..inlay_snapshot.to_inlay_offset(buffer_end) + }); + folds_cursor.next(&inlay_snapshot.buffer); item } }) @@ -353,7 +330,7 @@ impl FoldMap { let mut fold = folds.next().unwrap(); let sum = new_transforms.summary(); - assert!(fold.start >= sum.input.len); + assert!(fold.start.0 >= sum.input.len); while folds .peek() @@ -365,9 +342,9 @@ impl FoldMap { } } - if fold.start > sum.input.len { - let text_summary = new_buffer - .text_summary_for_range::(sum.input.len..fold.start); + if fold.start.0 > sum.input.len { + let text_summary = inlay_snapshot + .text_summary_for_range(InlayOffset(sum.input.len)..fold.start); new_transforms.push( Transform { summary: TransformSummary { @@ -386,7 +363,8 @@ impl FoldMap { Transform { summary: TransformSummary { output: TextSummary::from(output_text), - input: new_buffer.text_summary_for_range(fold.start..fold.end), + input: inlay_snapshot + .text_summary_for_range(fold.start..fold.end), }, output_text: Some(output_text), }, @@ -396,9 +374,9 @@ impl FoldMap { } let sum = new_transforms.summary(); - if sum.input.len < edit.new.end { - let text_summary = new_buffer - .text_summary_for_range::(sum.input.len..edit.new.end); + if sum.input.len < edit.new.end.0 { + let text_summary = inlay_snapshot + .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end); new_transforms.push( Transform { summary: TransformSummary { @@ -412,9 +390,9 @@ impl FoldMap { } } - new_transforms.push_tree(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(&()), &()); if new_transforms.is_empty() { - let text_summary = new_buffer.text_summary(); + let text_summary = inlay_snapshot.text_summary(); new_transforms.push( Transform { summary: TransformSummary { @@ -429,18 +407,21 @@ impl FoldMap { drop(cursor); - let mut fold_edits = Vec::with_capacity(buffer_edits.len()); + let mut fold_edits = Vec::with_capacity(inlay_edits.len()); { - let mut old_transforms = transforms.cursor::<(usize, FoldOffset)>(); - let mut new_transforms = new_transforms.cursor::<(usize, FoldOffset)>(); + let mut old_transforms = self + .snapshot + .transforms + .cursor::<(InlayOffset, FoldOffset)>(); + let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(); - for mut edit in buffer_edits { + for mut edit in inlay_edits { old_transforms.seek(&edit.old.start, Bias::Left, &()); if old_transforms.item().map_or(false, |t| t.is_fold()) { edit.old.start = old_transforms.start().0; } let old_start = - old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0); + old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0; old_transforms.seek_forward(&edit.old.end, Bias::Right, &()); if old_transforms.item().map_or(false, |t| t.is_fold()) { @@ -448,14 +429,14 @@ impl FoldMap { edit.old.end = old_transforms.start().0; } let old_end = - old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0); + old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0; new_transforms.seek(&edit.new.start, Bias::Left, &()); if new_transforms.item().map_or(false, |t| t.is_fold()) { edit.new.start = new_transforms.start().0; } let new_start = - new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0); + new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0; new_transforms.seek_forward(&edit.new.end, Bias::Right, &()); if new_transforms.item().map_or(false, |t| t.is_fold()) { @@ -463,7 +444,7 @@ impl FoldMap { edit.new.end = new_transforms.start().0; } let new_end = - new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0); + new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0; fold_edits.push(FoldEdit { old: FoldOffset(old_start)..FoldOffset(old_end), @@ -474,9 +455,9 @@ impl FoldMap { consolidate_fold_edits(&mut fold_edits); } - *transforms = new_transforms; - *self.buffer.lock() = new_buffer; - self.version.fetch_add(1, SeqCst); + self.snapshot.transforms = new_transforms; + self.snapshot.inlay_snapshot = inlay_snapshot; + self.snapshot.version += 1; fold_edits } } @@ -486,32 +467,28 @@ impl FoldMap { pub struct FoldSnapshot { transforms: SumTree, folds: SumTree, - buffer_snapshot: MultiBufferSnapshot, + pub inlay_snapshot: InlaySnapshot, pub version: usize, pub ellipses_color: Option, } impl FoldSnapshot { - pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { - &self.buffer_snapshot - } - #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None) .map(|c| c.text) .collect() } #[cfg(test)] pub fn fold_count(&self) -> usize { - self.folds.items(&self.buffer_snapshot).len() + self.folds.items(&self.inlay_snapshot.buffer).len() } pub fn text_summary_for_range(&self, range: Range) -> TextSummary { let mut summary = TextSummary::default(); - let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>(); + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(); cursor.seek(&range.start, Bias::Right, &()); if let Some(transform) = cursor.item() { let start_in_transform = range.start.0 - cursor.start().0 .0; @@ -522,11 +499,15 @@ impl FoldSnapshot { [start_in_transform.column as usize..end_in_transform.column as usize], ); } else { - let buffer_start = cursor.start().1 + start_in_transform; - let buffer_end = cursor.start().1 + end_in_transform; + let inlay_start = self + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform)); + let inlay_end = self + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform)); summary = self - .buffer_snapshot - .text_summary_for_range(buffer_start..buffer_end); + .inlay_snapshot + .text_summary_for_range(inlay_start..inlay_end); } } @@ -540,11 +521,13 @@ impl FoldSnapshot { if let Some(output_text) = transform.output_text { summary += TextSummary::from(&output_text[..end_in_transform.column as usize]); } else { - let buffer_start = cursor.start().1; - let buffer_end = cursor.start().1 + end_in_transform; + let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1); + let inlay_end = self + .inlay_snapshot + .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform)); summary += self - .buffer_snapshot - .text_summary_for_range::(buffer_start..buffer_end); + .inlay_snapshot + .text_summary_for_range(inlay_start..inlay_end); } } } @@ -552,8 +535,8 @@ impl FoldSnapshot { summary } - pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>(); + pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { + let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(); cursor.seek(&point, Bias::Right, &()); if cursor.item().map_or(false, |t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { @@ -562,7 +545,7 @@ impl FoldSnapshot { cursor.end(&()).1 } } else { - let overshoot = point - cursor.start().0; + let overshoot = point.0 - cursor.start().0 .0; FoldPoint(cmp::min( cursor.start().1 .0 + overshoot, cursor.end(&()).1 .0, @@ -590,12 +573,12 @@ impl FoldSnapshot { } let fold_point = FoldPoint::new(start_row, 0); - let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>(); + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(); cursor.seek(&fold_point, Bias::Left, &()); let overshoot = fold_point.0 - cursor.start().0 .0; - let buffer_point = cursor.start().1 + overshoot; - let input_buffer_rows = self.buffer_snapshot.buffer_rows(buffer_point.row); + let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot); + let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row()); FoldBufferRows { fold_point, @@ -617,10 +600,10 @@ impl FoldSnapshot { where T: ToOffset, { - let mut folds = intersecting_folds(&self.buffer_snapshot, &self.folds, range, false); + let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); iter::from_fn(move || { let item = folds.item().map(|f| &f.0); - folds.next(&self.buffer_snapshot); + folds.next(&self.inlay_snapshot.buffer); item }) } @@ -629,26 +612,39 @@ impl FoldSnapshot { where T: ToOffset, { - let offset = offset.to_offset(&self.buffer_snapshot); - let mut cursor = self.transforms.cursor::(); - cursor.seek(&offset, Bias::Right, &()); + let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer); + let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); + let mut cursor = self.transforms.cursor::(); + cursor.seek(&inlay_offset, Bias::Right, &()); cursor.item().map_or(false, |t| t.output_text.is_some()) } pub fn is_line_folded(&self, buffer_row: u32) -> bool { - let mut cursor = self.transforms.cursor::(); - cursor.seek(&Point::new(buffer_row, 0), Bias::Right, &()); - while let Some(transform) = cursor.item() { - if transform.output_text.is_some() { - return true; + let mut inlay_point = self + .inlay_snapshot + .to_inlay_point(Point::new(buffer_row, 0)); + let mut cursor = self.transforms.cursor::(); + cursor.seek(&inlay_point, Bias::Right, &()); + loop { + match cursor.item() { + Some(transform) => { + let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point); + if buffer_point.row != buffer_row { + return false; + } else if transform.output_text.is_some() { + return true; + } + } + None => return false, } - if cursor.end(&()).row == buffer_row { - cursor.next(&()) + + if cursor.end(&()).row() == inlay_point.row() { + cursor.next(&()); } else { - break; + inlay_point.0 += Point::new(1, 0); + cursor.seek(&inlay_point, Bias::Right, &()); } } - false } pub fn chunks<'a>( @@ -656,127 +652,56 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, + hint_highlights: Option, + suggestion_highlights: Option, ) -> FoldChunks<'a> { - let mut highlight_endpoints = Vec::new(); - let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>(); + let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); - let buffer_end = { + let inlay_end = { transform_cursor.seek(&range.end, Bias::Right, &()); let overshoot = range.end.0 - transform_cursor.start().0 .0; - transform_cursor.start().1 + overshoot + transform_cursor.start().1 + InlayOffset(overshoot) }; - let buffer_start = { + let inlay_start = { transform_cursor.seek(&range.start, Bias::Right, &()); let overshoot = range.start.0 - transform_cursor.start().0 .0; - transform_cursor.start().1 + overshoot + transform_cursor.start().1 + InlayOffset(overshoot) }; - if let Some(text_highlights) = text_highlights { - if !text_highlights.is_empty() { - while transform_cursor.start().0 < range.end { - if !transform_cursor.item().unwrap().is_fold() { - let transform_start = self - .buffer_snapshot - .anchor_after(cmp::max(buffer_start, transform_cursor.start().1)); - - let transform_end = { - let overshoot = range.end.0 - transform_cursor.start().0 .0; - self.buffer_snapshot.anchor_before(cmp::min( - transform_cursor.end(&()).1, - transform_cursor.start().1 + overshoot, - )) - }; - - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, self.buffer_snapshot()); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .start - .cmp(&transform_end, &self.buffer_snapshot) - .is_ge() - { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.start.to_offset(&self.buffer_snapshot), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.end.to_offset(&self.buffer_snapshot), - is_start: false, - tag: *tag, - style, - }); - } - } - } - - transform_cursor.next(&()); - } - highlight_endpoints.sort(); - transform_cursor.seek(&range.start, Bias::Right, &()); - } - } - FoldChunks { transform_cursor, - buffer_chunks: self - .buffer_snapshot - .chunks(buffer_start..buffer_end, language_aware), - buffer_chunk: None, - buffer_offset: buffer_start, + inlay_chunks: self.inlay_snapshot.chunks( + inlay_start..inlay_end, + language_aware, + text_highlights, + hint_highlights, + suggestion_highlights, + ), + inlay_chunk: None, + inlay_offset: inlay_start, output_offset: range.start.0, max_output_offset: range.end.0, - highlight_endpoints: highlight_endpoints.into_iter().peekable(), - active_highlights: Default::default(), ellipses_color: self.ellipses_color, } } + pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { + self.chunks(start.to_offset(self)..self.len(), false, None, None, None) + .flat_map(|chunk| chunk.text.chars()) + } + #[cfg(test)] pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset { - let mut cursor = self.transforms.cursor::<(FoldOffset, usize)>(); - cursor.seek(&offset, Bias::Right, &()); - if let Some(transform) = cursor.item() { - let transform_start = cursor.start().0 .0; - if transform.output_text.is_some() { - if offset.0 == transform_start || matches!(bias, Bias::Left) { - FoldOffset(transform_start) - } else { - FoldOffset(cursor.end(&()).0 .0) - } - } else { - let overshoot = offset.0 - transform_start; - let buffer_offset = cursor.start().1 + overshoot; - let clipped_buffer_offset = self.buffer_snapshot.clip_offset(buffer_offset, bias); - FoldOffset( - (offset.0 as isize + (clipped_buffer_offset as isize - buffer_offset as isize)) - as usize, - ) - } + if offset > self.len() { + self.len() } else { - FoldOffset(self.transforms.summary().output.len) + self.clip_point(offset.to_point(self), bias).to_offset(self) } } pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { - let mut cursor = self.transforms.cursor::<(FoldPoint, Point)>(); + let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(); cursor.seek(&point, Bias::Right, &()); if let Some(transform) = cursor.item() { let transform_start = cursor.start().0 .0; @@ -787,11 +712,10 @@ impl FoldSnapshot { FoldPoint(cursor.end(&()).0 .0) } } else { - let overshoot = point.0 - transform_start; - let buffer_position = cursor.start().1 + overshoot; - let clipped_buffer_position = - self.buffer_snapshot.clip_point(buffer_position, bias); - FoldPoint(cursor.start().0 .0 + (clipped_buffer_position - cursor.start().1)) + let overshoot = InlayPoint(point.0 - transform_start); + let inlay_point = cursor.start().1 + overshoot; + let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias); + FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0) } } else { FoldPoint(self.transforms.summary().output.lines) @@ -800,7 +724,7 @@ impl FoldSnapshot { } fn intersecting_folds<'a, T>( - buffer: &'a MultiBufferSnapshot, + inlay_snapshot: &'a InlaySnapshot, folds: &'a SumTree, range: Range, inclusive: bool, @@ -808,6 +732,7 @@ fn intersecting_folds<'a, T>( where T: ToOffset, { + let buffer = &inlay_snapshot.buffer; let start = buffer.anchor_before(range.start.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer)); let mut cursor = folds.filter::<_, usize>(move |summary| { @@ -824,7 +749,7 @@ where cursor } -fn consolidate_buffer_edits(edits: &mut Vec>) { +fn consolidate_inlay_edits(edits: &mut Vec) { edits.sort_unstable_by(|a, b| { a.old .start @@ -952,7 +877,7 @@ impl Default for FoldSummary { impl sum_tree::Summary for FoldSummary { type Context = MultiBufferSnapshot; - fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) { + fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less { self.min_start = other.min_start.clone(); } @@ -996,8 +921,8 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize { #[derive(Clone)] pub struct FoldBufferRows<'a> { - cursor: Cursor<'a, Transform, (FoldPoint, Point)>, - input_buffer_rows: MultiBufferRows<'a>, + cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>, + input_buffer_rows: InlayBufferRows<'a>, fold_point: FoldPoint, } @@ -1016,7 +941,7 @@ impl<'a> Iterator for FoldBufferRows<'a> { if self.cursor.item().is_some() { if traversed_fold { - self.input_buffer_rows.seek(self.cursor.start().1.row); + self.input_buffer_rows.seek(self.cursor.start().1.row()); self.input_buffer_rows.next(); } *self.fold_point.row_mut() += 1; @@ -1028,14 +953,12 @@ impl<'a> Iterator for FoldBufferRows<'a> { } pub struct FoldChunks<'a> { - transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>, - buffer_chunks: MultiBufferChunks<'a>, - buffer_chunk: Option<(usize, Chunk<'a>)>, - buffer_offset: usize, + transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>, + inlay_chunks: InlayChunks<'a>, + inlay_chunk: Option<(InlayOffset, Chunk<'a>)>, + inlay_offset: InlayOffset, output_offset: usize, max_output_offset: usize, - highlight_endpoints: Peekable>, - active_highlights: BTreeMap, HighlightStyle>, ellipses_color: Option, } @@ -1052,11 +975,11 @@ impl<'a> Iterator for FoldChunks<'a> { // If we're in a fold, then return the fold's display text and // advance the transform and buffer cursors to the end of the fold. if let Some(output_text) = transform.output_text { - self.buffer_chunk.take(); - self.buffer_offset += transform.summary.input.len; - self.buffer_chunks.seek(self.buffer_offset); + self.inlay_chunk.take(); + self.inlay_offset += InlayOffset(transform.summary.input.len); + self.inlay_chunks.seek(self.inlay_offset); - while self.buffer_offset >= self.transform_cursor.end(&()).1 + while self.inlay_offset >= self.transform_cursor.end(&()).1 && self.transform_cursor.item().is_some() { self.transform_cursor.next(&()); @@ -1073,53 +996,28 @@ impl<'a> Iterator for FoldChunks<'a> { }); } - let mut next_highlight_endpoint = usize::MAX; - while let Some(endpoint) = self.highlight_endpoints.peek().copied() { - if endpoint.offset <= self.buffer_offset { - if endpoint.is_start { - self.active_highlights.insert(endpoint.tag, endpoint.style); - } else { - self.active_highlights.remove(&endpoint.tag); - } - self.highlight_endpoints.next(); - } else { - next_highlight_endpoint = endpoint.offset; - break; - } - } - // Retrieve a chunk from the current location in the buffer. - if self.buffer_chunk.is_none() { - let chunk_offset = self.buffer_chunks.offset(); - self.buffer_chunk = self.buffer_chunks.next().map(|chunk| (chunk_offset, chunk)); + if self.inlay_chunk.is_none() { + let chunk_offset = self.inlay_chunks.offset(); + self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk)); } // Otherwise, take a chunk from the buffer's text. - if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk { - let buffer_chunk_end = buffer_chunk_start + chunk.text.len(); + if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk { + let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len()); let transform_end = self.transform_cursor.end(&()).1; - let chunk_end = buffer_chunk_end - .min(transform_end) - .min(next_highlight_endpoint); + let chunk_end = buffer_chunk_end.min(transform_end); chunk.text = &chunk.text - [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start]; - - if !self.active_highlights.is_empty() { - let mut highlight_style = HighlightStyle::default(); - for active_highlight in self.active_highlights.values() { - highlight_style.highlight(*active_highlight); - } - chunk.highlight_style = Some(highlight_style); - } + [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0]; if chunk_end == transform_end { self.transform_cursor.next(&()); } else if chunk_end == buffer_chunk_end { - self.buffer_chunk.take(); + self.inlay_chunk.take(); } - self.buffer_offset = chunk_end; + self.inlay_offset = chunk_end; self.output_offset += chunk.text.len(); return Some(chunk); } @@ -1130,7 +1028,7 @@ impl<'a> Iterator for FoldChunks<'a> { #[derive(Copy, Clone, Eq, PartialEq)] struct HighlightEndpoint { - offset: usize, + offset: InlayOffset, is_start: bool, tag: Option, style: HighlightStyle, @@ -1162,12 +1060,34 @@ impl FoldOffset { let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) { Point::new(0, (self.0 - cursor.start().0 .0) as u32) } else { - let buffer_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0; - let buffer_point = snapshot.buffer_snapshot.offset_to_point(buffer_offset); - buffer_point - cursor.start().1.input.lines + let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0; + let inlay_point = snapshot.inlay_snapshot.to_point(InlayOffset(inlay_offset)); + inlay_point.0 - cursor.start().1.input.lines }; FoldPoint(cursor.start().1.output.lines + overshoot) } + + #[cfg(test)] + pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { + let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(); + cursor.seek(&self, Bias::Right, &()); + let overshoot = self.0 - cursor.start().0 .0; + InlayOffset(cursor.start().1 .0 + overshoot) + } +} + +impl Add for FoldOffset { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for FoldOffset { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } } impl Sub for FoldOffset { @@ -1184,15 +1104,15 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset { } } -impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint { fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - *self += &summary.input.lines; + self.0 += &summary.input.lines; } } -impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize { +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset { fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - *self += &summary.input.len; + self.0 += &summary.input.len; } } @@ -1201,12 +1121,12 @@ pub type FoldEdit = Edit; #[cfg(test)] mod tests { use super::*; - use crate::{MultiBuffer, ToPoint}; + use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint}; use collections::HashSet; use rand::prelude::*; use settings::SettingsStore; - use std::{cmp::Reverse, env, mem, sync::Arc}; - use sum_tree::TreeMap; + use std::{env, mem}; + use text::Patch; use util::test::sample_text; use util::RandomCharIter; use Bias::{Left, Right}; @@ -1217,9 +1137,10 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let mut map = FoldMap::new(buffer_snapshot.clone()).0; + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut writer, _, _) = map.write(buffer_snapshot, vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); let (snapshot2, edits) = writer.fold(vec![ Point::new(0, 2)..Point::new(2, 2), Point::new(2, 4)..Point::new(4, 1), @@ -1250,7 +1171,10 @@ mod tests { ); buffer.snapshot(cx) }); - let (snapshot3, edits) = map.read(buffer_snapshot, subscription.consume().into_inner()); + + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot3, edits) = map.read(inlay_snapshot, inlay_edits); assert_eq!(snapshot3.text(), "123a⋯c123c⋯eeeee"); assert_eq!( edits, @@ -1270,17 +1194,19 @@ mod tests { buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx); buffer.snapshot(cx) }); - let (snapshot4, _) = map.read(buffer_snapshot.clone(), subscription.consume().into_inner()); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot4, _) = map.read(inlay_snapshot.clone(), inlay_edits); assert_eq!(snapshot4.text(), "123a⋯c123456eee"); - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false); - let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]); + let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot5.text(), "123a⋯c123456eee"); - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true); - let (snapshot6, _) = map.read(buffer_snapshot, vec![]); + let (snapshot6, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee"); } @@ -1290,35 +1216,36 @@ mod tests { let buffer = MultiBuffer::build_simple("abcdefghijkl", cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); { - let mut map = FoldMap::new(buffer_snapshot.clone()).0; + let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![5..8]); - let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "abcde⋯ijkl"); // Create an fold adjacent to the start of the first fold. - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![0..1, 2..5]); - let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "⋯b⋯ijkl"); // Create an fold adjacent to the end of the first fold. - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![11..11, 8..10]); - let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "⋯b⋯kl"); } { - let mut map = FoldMap::new(buffer_snapshot.clone()).0; + let mut map = FoldMap::new(inlay_snapshot.clone()).0; // Create two adjacent folds. - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![0..2, 2..5]); - let (snapshot, _) = map.read(buffer_snapshot, vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "⋯fghijkl"); // Edit within one of the folds. @@ -1326,7 +1253,9 @@ mod tests { buffer.edit([(0..1, "12345")], None, cx); buffer.snapshot(cx) }); - let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner()); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot, _) = map.read(inlay_snapshot, inlay_edits); assert_eq!(snapshot.text(), "12345⋯fghijkl"); } } @@ -1335,15 +1264,16 @@ mod tests { fn test_overlapping_folds(cx: &mut gpui::AppContext) { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let mut map = FoldMap::new(buffer_snapshot.clone()).0; - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ Point::new(0, 2)..Point::new(2, 2), Point::new(0, 4)..Point::new(1, 0), Point::new(1, 2)..Point::new(3, 2), Point::new(3, 1)..Point::new(4, 1), ]); - let (snapshot, _) = map.read(buffer_snapshot, vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯eeeee"); } @@ -1353,21 +1283,24 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let subscription = buffer.update(cx, |buffer, _| buffer.subscribe()); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let mut map = FoldMap::new(buffer_snapshot.clone()).0; + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ Point::new(0, 2)..Point::new(2, 2), Point::new(3, 1)..Point::new(4, 1), ]); - let (snapshot, _) = map.read(buffer_snapshot, vec![]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); let buffer_snapshot = buffer.update(cx, |buffer, cx| { buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx); buffer.snapshot(cx) }); - let (snapshot, _) = map.read(buffer_snapshot, subscription.consume().into_inner()); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot, subscription.consume().into_inner()); + let (snapshot, _) = map.read(inlay_snapshot, inlay_edits); assert_eq!(snapshot.text(), "aa⋯eeeee"); } @@ -1375,16 +1308,17 @@ mod tests { fn test_folds_in_range(cx: &mut gpui::AppContext) { let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let mut map = FoldMap::new(buffer_snapshot.clone()).0; + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ Point::new(0, 2)..Point::new(2, 2), Point::new(0, 4)..Point::new(1, 0), Point::new(1, 2)..Point::new(3, 2), Point::new(3, 1)..Point::new(4, 1), ]); - let (snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); + let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let fold_ranges = snapshot .folds_in_range(Point::new(1, 0)..Point::new(1, 3)) .map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot)) @@ -1413,37 +1347,25 @@ mod tests { MultiBuffer::build_random(&mut rng, cx) }; let mut buffer_snapshot = buffer.read(cx).snapshot(cx); - let mut map = FoldMap::new(buffer_snapshot.clone()).0; + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut initial_snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); + let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let mut snapshot_edits = Vec::new(); - let mut highlights = TreeMap::default(); - let highlight_count = rng.gen_range(0_usize..10); - let mut highlight_ranges = (0..highlight_count) - .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) - .collect::>(); - highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); - log::info!("highlighting ranges {:?}", highlight_ranges); - let highlight_ranges = highlight_ranges - .into_iter() - .map(|range| { - buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) - }) - .collect::>(); - - highlights.insert( - Some(TypeId::of::<()>()), - Arc::new((HighlightStyle::default(), highlight_ranges)), - ); - + let mut next_inlay_id = 0; for _ in 0..operations { log::info!("text: {:?}", buffer_snapshot.text()); let mut buffer_edits = Vec::new(); + let mut inlay_edits = Vec::new(); match rng.gen_range(0..=100) { - 0..=59 => { + 0..=39 => { snapshot_edits.extend(map.randomly_mutate(&mut rng)); } + 40..=59 => { + let (_, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + inlay_edits = edits; + } _ => buffer.update(cx, |buffer, cx| { let subscription = buffer.subscribe(); let edit_count = rng.gen_range(1..=5); @@ -1455,12 +1377,21 @@ mod tests { }), }; - let (snapshot, edits) = map.read(buffer_snapshot.clone(), buffer_edits); + let (inlay_snapshot, new_inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + log::info!("inlay text {:?}", inlay_snapshot.text()); + + let inlay_edits = Patch::new(inlay_edits) + .compose(new_inlay_edits) + .into_inner(); + let (snapshot, edits) = map.read(inlay_snapshot.clone(), inlay_edits); snapshot_edits.push((snapshot.clone(), edits)); - let mut expected_text: String = buffer_snapshot.text().to_string(); + let mut expected_text: String = inlay_snapshot.text().to_string(); for fold_range in map.merged_fold_ranges().into_iter().rev() { - expected_text.replace_range(fold_range.start..fold_range.end, "⋯"); + let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start); + let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end); + expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "⋯"); } assert_eq!(snapshot.text(), expected_text); @@ -1473,16 +1404,20 @@ mod tests { let mut prev_row = 0; let mut expected_buffer_rows = Vec::new(); for fold_range in map.merged_fold_ranges().into_iter() { - let fold_start = buffer_snapshot.offset_to_point(fold_range.start).row; - let fold_end = buffer_snapshot.offset_to_point(fold_range.end).row; + let fold_start = inlay_snapshot + .to_point(inlay_snapshot.to_inlay_offset(fold_range.start)) + .row(); + let fold_end = inlay_snapshot + .to_point(inlay_snapshot.to_inlay_offset(fold_range.end)) + .row(); expected_buffer_rows.extend( - buffer_snapshot + inlay_snapshot .buffer_rows(prev_row) .take((1 + fold_start - prev_row) as usize), ); prev_row = 1 + fold_end; } - expected_buffer_rows.extend(buffer_snapshot.buffer_rows(prev_row)); + expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row)); assert_eq!( expected_buffer_rows.len(), @@ -1508,19 +1443,19 @@ mod tests { let mut fold_offset = FoldOffset(0); let mut char_column = 0; for c in expected_text.chars() { - let buffer_point = fold_point.to_buffer_point(&snapshot); - let buffer_offset = buffer_point.to_offset(&buffer_snapshot); + let inlay_point = fold_point.to_inlay_point(&snapshot); + let inlay_offset = fold_offset.to_inlay_offset(&snapshot); assert_eq!( - snapshot.to_fold_point(buffer_point, Right), + snapshot.to_fold_point(inlay_point, Right), fold_point, "{:?} -> fold point", - buffer_point, + inlay_point, ); assert_eq!( - fold_point.to_buffer_offset(&snapshot), - buffer_offset, - "fold_point.to_buffer_offset({:?})", - fold_point, + inlay_snapshot.to_offset(inlay_point), + inlay_offset, + "inlay_snapshot.to_offset({:?})", + inlay_point, ); assert_eq!( fold_point.to_offset(&snapshot), @@ -1561,7 +1496,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, Some(&highlights)) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), text, @@ -1570,9 +1505,6 @@ mod tests { let mut fold_row = 0; while fold_row < expected_buffer_rows.len() as u32 { - fold_row = snapshot - .clip_point(FoldPoint::new(fold_row, 0), Bias::Right) - .row(); assert_eq!( snapshot.buffer_rows(fold_row).collect::>(), expected_buffer_rows[(fold_row as usize)..], @@ -1582,13 +1514,31 @@ mod tests { fold_row += 1; } - let fold_start_rows = map + let folded_buffer_rows = map .merged_fold_ranges() .iter() - .map(|range| range.start.to_point(&buffer_snapshot).row) + .flat_map(|range| { + let start_row = range.start.to_point(&buffer_snapshot).row; + let end = range.end.to_point(&buffer_snapshot); + if end.column == 0 { + start_row..end.row + } else { + start_row..end.row + 1 + } + }) .collect::>(); - for row in fold_start_rows { - assert!(snapshot.is_line_folded(row)); + for row in 0..=buffer_snapshot.max_point().row { + assert_eq!( + snapshot.is_line_folded(row), + folded_buffer_rows.contains(&row), + "expected buffer row {}{} to be folded", + row, + if folded_buffer_rows.contains(&row) { + "" + } else { + " not" + } + ); } for _ in 0..5 { @@ -1596,6 +1546,7 @@ mod tests { buffer_snapshot.clip_offset(rng.gen_range(0..=buffer_snapshot.len()), Right); let start = buffer_snapshot.clip_offset(rng.gen_range(0..=end), Left); let expected_folds = map + .snapshot .folds .items(&buffer_snapshot) .into_iter() @@ -1659,15 +1610,16 @@ mod tests { let buffer = MultiBuffer::build_simple(&text, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let mut map = FoldMap::new(buffer_snapshot.clone()).0; + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot); + let mut map = FoldMap::new(inlay_snapshot.clone()).0; - let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ Point::new(0, 2)..Point::new(2, 2), Point::new(3, 1)..Point::new(4, 1), ]); - let (snapshot, _) = map.read(buffer_snapshot, vec![]); + let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee\nffffff\n"); assert_eq!( snapshot.buffer_rows(0).collect::>(), @@ -1682,13 +1634,14 @@ mod tests { impl FoldMap { fn merged_fold_ranges(&self) -> Vec> { - let buffer = self.buffer.lock().clone(); - let mut folds = self.folds.items(&buffer); + let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); + let buffer = &inlay_snapshot.buffer; + let mut folds = self.snapshot.folds.items(buffer); // Ensure sorting doesn't change how folds get merged and displayed. - folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer)); + folds.sort_by(|a, b| a.0.cmp(&b.0, buffer)); let mut fold_ranges = folds .iter() - .map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer)) + .map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer)) .peekable(); let mut merged_ranges = Vec::new(); @@ -1716,8 +1669,9 @@ mod tests { ) -> Vec<(FoldSnapshot, Vec)> { let mut snapshot_edits = Vec::new(); match rng.gen_range(0..=100) { - 0..=39 if !self.folds.is_empty() => { - let buffer = self.buffer.lock().clone(); + 0..=39 if !self.snapshot.folds.is_empty() => { + let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); + let buffer = &inlay_snapshot.buffer; let mut to_unfold = Vec::new(); for _ in 0..rng.gen_range(1..=3) { let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); @@ -1725,13 +1679,14 @@ mod tests { to_unfold.push(start..end); } log::info!("unfolding {:?}", to_unfold); - let (mut writer, snapshot, edits) = self.write(buffer, vec![]); + let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); snapshot_edits.push((snapshot, edits)); let (snapshot, edits) = writer.fold(to_unfold); snapshot_edits.push((snapshot, edits)); } _ => { - let buffer = self.buffer.lock().clone(); + let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); + let buffer = &inlay_snapshot.buffer; let mut to_fold = Vec::new(); for _ in 0..rng.gen_range(1..=2) { let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); @@ -1739,7 +1694,7 @@ mod tests { to_fold.push(start..end); } log::info!("folding {:?}", to_fold); - let (mut writer, snapshot, edits) = self.write(buffer, vec![]); + let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); snapshot_edits.push((snapshot, edits)); let (snapshot, edits) = writer.fold(to_fold); snapshot_edits.push((snapshot, edits)); diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs new file mode 100644 index 0000000000..6a59cecae8 --- /dev/null +++ b/crates/editor/src/display_map/inlay_map.rs @@ -0,0 +1,1787 @@ +use crate::{ + multi_buffer::{MultiBufferChunks, MultiBufferRows}, + Anchor, InlayId, MultiBufferSnapshot, ToOffset, +}; +use collections::{BTreeMap, BTreeSet}; +use gpui::fonts::HighlightStyle; +use language::{Chunk, Edit, Point, TextSummary}; +use std::{ + any::TypeId, + cmp, + iter::Peekable, + ops::{Add, AddAssign, Range, Sub, SubAssign}, + vec, +}; +use sum_tree::{Bias, Cursor, SumTree}; +use text::{Patch, Rope}; + +use super::TextHighlights; + +pub struct InlayMap { + snapshot: InlaySnapshot, + inlays: Vec, +} + +#[derive(Clone)] +pub struct InlaySnapshot { + pub buffer: MultiBufferSnapshot, + transforms: SumTree, + pub version: usize, +} + +#[derive(Clone, Debug)] +enum Transform { + Isomorphic(TextSummary), + Inlay(Inlay), +} + +#[derive(Debug, Clone)] +pub struct Inlay { + pub id: InlayId, + pub position: Anchor, + pub text: text::Rope, +} + +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>(id: usize, position: Anchor, text: T) -> Self { + Self { + id: InlayId::Suggestion(id), + position, + text: text.into(), + } + } +} + +impl sum_tree::Item for Transform { + type Summary = TransformSummary; + + fn summary(&self) -> Self::Summary { + match self { + Transform::Isomorphic(summary) => TransformSummary { + input: summary.clone(), + output: summary.clone(), + }, + Transform::Inlay(inlay) => TransformSummary { + input: TextSummary::default(), + output: inlay.text.summary(), + }, + } + } +} + +#[derive(Clone, Debug, Default)] +struct TransformSummary { + input: TextSummary, + output: TextSummary, +} + +impl sum_tree::Summary for TransformSummary { + type Context = (); + + fn add_summary(&mut self, other: &Self, _: &()) { + self.input += &other.input; + self.output += &other.output; + } +} + +pub type InlayEdit = Edit; + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct InlayOffset(pub usize); + +impl Add for InlayOffset { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for InlayOffset { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl AddAssign for InlayOffset { + fn add_assign(&mut self, rhs: Self) { + self.0 += rhs.0; + } +} + +impl SubAssign for InlayOffset { + fn sub_assign(&mut self, rhs: Self) { + self.0 -= rhs.0; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.output.len; + } +} + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct InlayPoint(pub Point); + +impl Add for InlayPoint { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for InlayPoint { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.output.lines; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + *self += &summary.input.len; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + *self += &summary.input.lines; + } +} + +#[derive(Clone)] +pub struct InlayBufferRows<'a> { + transforms: Cursor<'a, Transform, (InlayPoint, Point)>, + buffer_rows: MultiBufferRows<'a>, + inlay_row: u32, + max_buffer_row: u32, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +struct HighlightEndpoint { + offset: InlayOffset, + is_start: bool, + tag: Option, + style: HighlightStyle, +} + +impl PartialOrd for HighlightEndpoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for HighlightEndpoint { + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.offset + .cmp(&other.offset) + .then_with(|| other.is_start.cmp(&self.is_start)) + } +} + +pub struct InlayChunks<'a> { + transforms: Cursor<'a, Transform, (InlayOffset, usize)>, + buffer_chunks: MultiBufferChunks<'a>, + buffer_chunk: Option>, + inlay_chunks: Option>, + output_offset: InlayOffset, + max_output_offset: InlayOffset, + hint_highlight_style: Option, + suggestion_highlight_style: Option, + highlight_endpoints: Peekable>, + active_highlights: BTreeMap, HighlightStyle>, + snapshot: &'a InlaySnapshot, +} + +impl<'a> InlayChunks<'a> { + pub fn seek(&mut self, offset: InlayOffset) { + self.transforms.seek(&offset, Bias::Right, &()); + + let buffer_offset = self.snapshot.to_buffer_offset(offset); + self.buffer_chunks.seek(buffer_offset); + self.inlay_chunks = None; + self.buffer_chunk = None; + self.output_offset = offset; + } + + pub fn offset(&self) -> InlayOffset { + self.output_offset + } +} + +impl<'a> Iterator for InlayChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.output_offset == self.max_output_offset { + return None; + } + + let mut next_highlight_endpoint = InlayOffset(usize::MAX); + while let Some(endpoint) = self.highlight_endpoints.peek().copied() { + if endpoint.offset <= self.output_offset { + if endpoint.is_start { + self.active_highlights.insert(endpoint.tag, endpoint.style); + } else { + self.active_highlights.remove(&endpoint.tag); + } + self.highlight_endpoints.next(); + } else { + next_highlight_endpoint = endpoint.offset; + break; + } + } + + let chunk = match self.transforms.item()? { + Transform::Isomorphic(_) => { + let chunk = self + .buffer_chunk + .get_or_insert_with(|| self.buffer_chunks.next().unwrap()); + if chunk.text.is_empty() { + *chunk = self.buffer_chunks.next().unwrap(); + } + + let (prefix, suffix) = chunk.text.split_at( + chunk + .text + .len() + .min(self.transforms.end(&()).0 .0 - self.output_offset.0) + .min(next_highlight_endpoint.0 - self.output_offset.0), + ); + + chunk.text = suffix; + self.output_offset.0 += prefix.len(); + let mut prefix = Chunk { + text: prefix, + ..chunk.clone() + }; + if !self.active_highlights.is_empty() { + let mut highlight_style = HighlightStyle::default(); + for active_highlight in self.active_highlights.values() { + highlight_style.highlight(*active_highlight); + } + prefix.highlight_style = Some(highlight_style); + } + prefix + } + Transform::Inlay(inlay) => { + let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| { + let start = self.output_offset - self.transforms.start().0; + let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0) + - self.transforms.start().0; + inlay.text.chunks_in_range(start.0..end.0) + }); + + let chunk = inlay_chunks.next().unwrap(); + self.output_offset.0 += chunk.len(); + let highlight_style = match inlay.id { + InlayId::Suggestion(_) => self.suggestion_highlight_style, + InlayId::Hint(_) => self.hint_highlight_style, + }; + Chunk { + text: chunk, + highlight_style, + ..Default::default() + } + } + }; + + if self.output_offset == self.transforms.end(&()).0 { + self.inlay_chunks = None; + self.transforms.next(&()); + } + + Some(chunk) + } +} + +impl<'a> InlayBufferRows<'a> { + pub fn seek(&mut self, row: u32) { + let inlay_point = InlayPoint::new(row, 0); + self.transforms.seek(&inlay_point, Bias::Left, &()); + + let mut buffer_point = self.transforms.start().1; + let buffer_row = if row == 0 { + 0 + } else { + match self.transforms.item() { + Some(Transform::Isomorphic(_)) => { + buffer_point += inlay_point.0 - self.transforms.start().0 .0; + buffer_point.row + } + _ => cmp::min(buffer_point.row + 1, self.max_buffer_row), + } + }; + self.inlay_row = inlay_point.row(); + self.buffer_rows.seek(buffer_row); + } +} + +impl<'a> Iterator for InlayBufferRows<'a> { + type Item = Option; + + fn next(&mut self) -> Option { + let buffer_row = if self.inlay_row == 0 { + self.buffer_rows.next().unwrap() + } else { + match self.transforms.item()? { + Transform::Inlay(_) => None, + Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(), + } + }; + + self.inlay_row += 1; + self.transforms + .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &()); + + Some(buffer_row) + } +} + +impl InlayPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(Point::new(row, column)) + } + + pub fn row(self) -> u32 { + self.0.row + } +} + +impl InlayMap { + pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) { + let version = 0; + let snapshot = InlaySnapshot { + buffer: buffer.clone(), + transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()), + version, + }; + + ( + Self { + snapshot: snapshot.clone(), + inlays: Vec::new(), + }, + snapshot, + ) + } + + pub fn sync( + &mut self, + buffer_snapshot: MultiBufferSnapshot, + mut buffer_edits: Vec>, + ) -> (InlaySnapshot, Vec) { + let mut snapshot = &mut self.snapshot; + + if buffer_edits.is_empty() { + if snapshot.buffer.trailing_excerpt_update_count() + != buffer_snapshot.trailing_excerpt_update_count() + { + buffer_edits.push(Edit { + old: snapshot.buffer.len()..snapshot.buffer.len(), + new: buffer_snapshot.len()..buffer_snapshot.len(), + }); + } + } + + if buffer_edits.is_empty() { + if snapshot.buffer.edit_count() != buffer_snapshot.edit_count() + || snapshot.buffer.parse_count() != buffer_snapshot.parse_count() + || snapshot.buffer.diagnostics_update_count() + != buffer_snapshot.diagnostics_update_count() + || snapshot.buffer.git_diff_update_count() + != buffer_snapshot.git_diff_update_count() + || snapshot.buffer.trailing_excerpt_update_count() + != buffer_snapshot.trailing_excerpt_update_count() + { + snapshot.version += 1; + } + + snapshot.buffer = buffer_snapshot; + (snapshot.clone(), Vec::new()) + } else { + let mut inlay_edits = Patch::default(); + let mut new_transforms = SumTree::new(); + let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(); + let mut buffer_edits_iter = buffer_edits.iter().peekable(); + while let Some(buffer_edit) = buffer_edits_iter.next() { + new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &()); + if let Some(Transform::Isomorphic(transform)) = cursor.item() { + if cursor.end(&()).0 == buffer_edit.old.start { + push_isomorphic(&mut new_transforms, transform.clone()); + cursor.next(&()); + } + } + + // Remove all the inlays and transforms contained by the edit. + let old_start = + cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0); + cursor.seek(&buffer_edit.old.end, Bias::Right, &()); + let old_end = + cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0); + + // Push the unchanged prefix. + let prefix_start = new_transforms.summary().input.len; + let prefix_end = buffer_edit.new.start; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(prefix_start..prefix_end), + ); + let new_start = InlayOffset(new_transforms.summary().output.len); + + let start_ix = match self.inlays.binary_search_by(|probe| { + probe + .position + .to_offset(&buffer_snapshot) + .cmp(&buffer_edit.new.start) + .then(std::cmp::Ordering::Greater) + }) { + Ok(ix) | Err(ix) => ix, + }; + + for inlay in &self.inlays[start_ix..] { + let buffer_offset = inlay.position.to_offset(&buffer_snapshot); + if buffer_offset > buffer_edit.new.end { + break; + } + + let prefix_start = new_transforms.summary().input.len; + let prefix_end = buffer_offset; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(prefix_start..prefix_end), + ); + + if inlay.position.is_valid(&buffer_snapshot) { + new_transforms.push(Transform::Inlay(inlay.clone()), &()); + } + } + + // Apply the rest of the edit. + let transform_start = new_transforms.summary().input.len; + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end), + ); + let new_end = InlayOffset(new_transforms.summary().output.len); + inlay_edits.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + // If the next edit doesn't intersect the current isomorphic transform, then + // we can push its remainder. + if buffer_edits_iter + .peek() + .map_or(true, |edit| edit.old.start >= cursor.end(&()).0) + { + let transform_start = new_transforms.summary().input.len; + let transform_end = + buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end); + push_isomorphic( + &mut new_transforms, + buffer_snapshot.text_summary_for_range(transform_start..transform_end), + ); + cursor.next(&()); + } + } + + new_transforms.append(cursor.suffix(&()), &()); + if new_transforms.is_empty() { + new_transforms.push(Transform::Isomorphic(Default::default()), &()); + } + + drop(cursor); + snapshot.transforms = new_transforms; + snapshot.version += 1; + snapshot.buffer = buffer_snapshot; + snapshot.check_invariants(); + + (snapshot.clone(), inlay_edits.into_inner()) + } + } + + pub fn splice( + &mut self, + to_remove: Vec, + to_insert: Vec, + ) -> (InlaySnapshot, Vec) { + let snapshot = &mut self.snapshot; + let mut edits = BTreeSet::new(); + + self.inlays.retain(|inlay| { + let retain = !to_remove.contains(&inlay.id); + if !retain { + let offset = inlay.position.to_offset(&snapshot.buffer); + edits.insert(offset); + } + retain + }); + + for inlay_to_insert in to_insert { + // Avoid inserting empty inlays. + if inlay_to_insert.text.is_empty() { + continue; + } + + 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_to_insert); + } + } + + edits.insert(offset); + } + + let buffer_edits = edits + .into_iter() + .map(|offset| Edit { + old: offset..offset, + new: offset..offset, + }) + .collect(); + let buffer_snapshot = snapshot.buffer.clone(); + drop(snapshot); + let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits); + (snapshot, edits) + } + + pub fn current_inlays(&self) -> impl Iterator { + self.inlays.iter() + } + + #[cfg(test)] + pub(crate) fn randomly_mutate( + &mut self, + next_inlay_id: &mut usize, + rng: &mut rand::rngs::StdRng, + ) -> (InlaySnapshot, Vec) { + use rand::prelude::*; + use util::post_inc; + + let mut to_remove = Vec::new(); + let mut to_insert = Vec::new(); + let snapshot = &mut self.snapshot; + for i in 0..rng.gen_range(1..=5) { + if self.inlays.is_empty() || rng.gen() { + let position = snapshot.buffer.random_byte_range(0, rng).start; + let bias = if rng.gen() { Bias::Left } else { Bias::Right }; + let len = if rng.gen_bool(0.01) { + 0 + } else { + rng.gen_range(1..=5) + }; + let text = util::RandomCharIter::new(&mut *rng) + .filter(|ch| *ch != '\r') + .take(len) + .collect::(); + log::info!( + "creating inlay at buffer offset {} with bias {:?} and text {:?}", + position, + bias, + text + ); + + let inlay_id = if i % 2 == 0 { + InlayId::Hint(post_inc(next_inlay_id)) + } else { + InlayId::Suggestion(post_inc(next_inlay_id)) + }; + to_insert.push(Inlay { + id: inlay_id, + position: snapshot.buffer.anchor_at(position, bias), + text: text.into(), + }); + } else { + to_remove.push( + self.inlays + .iter() + .choose(rng) + .map(|inlay| inlay.id) + .unwrap(), + ); + } + } + log::info!("removing inlays: {:?}", to_remove); + + drop(snapshot); + let (snapshot, edits) = self.splice(to_remove, to_insert); + (snapshot, edits) + } +} + +impl InlaySnapshot { + pub fn to_point(&self, offset: InlayOffset) -> InlayPoint { + let mut cursor = self + .transforms + .cursor::<(InlayOffset, (InlayPoint, usize))>(); + cursor.seek(&offset, Bias::Right, &()); + let overshoot = offset.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let buffer_offset_start = cursor.start().1 .1; + let buffer_offset_end = buffer_offset_start + overshoot; + let buffer_start = self.buffer.offset_to_point(buffer_offset_start); + let buffer_end = self.buffer.offset_to_point(buffer_offset_end); + InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start)) + } + Some(Transform::Inlay(inlay)) => { + let overshoot = inlay.text.offset_to_point(overshoot); + InlayPoint(cursor.start().1 .0 .0 + overshoot) + } + None => self.max_point(), + } + } + + pub fn len(&self) -> InlayOffset { + InlayOffset(self.transforms.summary().output.len) + } + + pub fn max_point(&self) -> InlayPoint { + InlayPoint(self.transforms.summary().output.lines) + } + + pub fn to_offset(&self, point: InlayPoint) -> InlayOffset { + let mut cursor = self + .transforms + .cursor::<(InlayPoint, (InlayOffset, Point))>(); + cursor.seek(&point, Bias::Right, &()); + let overshoot = point.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let buffer_point_start = cursor.start().1 .1; + let buffer_point_end = buffer_point_start + overshoot; + let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start); + let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end); + InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start)) + } + Some(Transform::Inlay(inlay)) => { + let overshoot = inlay.text.point_to_offset(overshoot); + InlayOffset(cursor.start().1 .0 .0 + overshoot) + } + None => self.len(), + } + } + + pub fn to_buffer_point(&self, point: InlayPoint) -> Point { + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(); + cursor.seek(&point, Bias::Right, &()); + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let overshoot = point.0 - cursor.start().0 .0; + cursor.start().1 + overshoot + } + Some(Transform::Inlay(_)) => cursor.start().1, + None => self.buffer.max_point(), + } + } + + pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); + cursor.seek(&offset, Bias::Right, &()); + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let overshoot = offset - cursor.start().0; + cursor.start().1 + overshoot.0 + } + Some(Transform::Inlay(_)) => cursor.start().1, + None => self.buffer.len(), + } + } + + pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { + let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(); + cursor.seek(&offset, Bias::Left, &()); + loop { + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + if offset == cursor.end(&()).0 { + while let Some(Transform::Inlay(inlay)) = cursor.next_item() { + if inlay.position.bias() == Bias::Right { + break; + } else { + cursor.next(&()); + } + } + return cursor.end(&()).1; + } else { + let overshoot = offset - cursor.start().0; + return InlayOffset(cursor.start().1 .0 + overshoot); + } + } + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Left { + cursor.next(&()); + } else { + return cursor.start().1; + } + } + None => { + return self.len(); + } + } + } + } + + pub fn to_inlay_point(&self, point: Point) -> InlayPoint { + let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(); + cursor.seek(&point, Bias::Left, &()); + loop { + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + if point == cursor.end(&()).0 { + while let Some(Transform::Inlay(inlay)) = cursor.next_item() { + if inlay.position.bias() == Bias::Right { + break; + } else { + cursor.next(&()); + } + } + return cursor.end(&()).1; + } else { + let overshoot = point - cursor.start().0; + return InlayPoint(cursor.start().1 .0 + overshoot); + } + } + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Left { + cursor.next(&()); + } else { + return cursor.start().1; + } + } + None => { + return self.max_point(); + } + } + } + } + + pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(); + cursor.seek(&point, Bias::Left, &()); + loop { + match cursor.item() { + Some(Transform::Isomorphic(transform)) => { + if cursor.start().0 == point { + if let Some(Transform::Inlay(inlay)) = cursor.prev_item() { + if inlay.position.bias() == Bias::Left { + return point; + } else if bias == Bias::Left { + cursor.prev(&()); + } else if transform.first_line_chars == 0 { + point.0 += Point::new(1, 0); + } else { + point.0 += Point::new(0, 1); + } + } else { + return point; + } + } else if cursor.end(&()).0 == point { + if let Some(Transform::Inlay(inlay)) = cursor.next_item() { + if inlay.position.bias() == Bias::Right { + return point; + } else if bias == Bias::Right { + cursor.next(&()); + } else if point.0.column == 0 { + point.0.row -= 1; + point.0.column = self.line_len(point.0.row); + } else { + point.0.column -= 1; + } + } else { + return point; + } + } else { + let overshoot = point.0 - cursor.start().0 .0; + let buffer_point = cursor.start().1 + overshoot; + let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias); + let clipped_overshoot = clipped_buffer_point - cursor.start().1; + let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot); + if clipped_point == point { + return clipped_point; + } else { + point = clipped_point; + } + } + } + Some(Transform::Inlay(inlay)) => { + if point == cursor.start().0 && inlay.position.bias() == Bias::Right { + match cursor.prev_item() { + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Left { + return point; + } + } + _ => return point, + } + } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left { + match cursor.next_item() { + Some(Transform::Inlay(inlay)) => { + if inlay.position.bias() == Bias::Right { + return point; + } + } + _ => return point, + } + } + + if bias == Bias::Left { + point = cursor.start().0; + cursor.prev(&()); + } else { + cursor.next(&()); + point = cursor.start().0; + } + } + None => { + bias = bias.invert(); + if bias == Bias::Left { + point = cursor.start().0; + cursor.prev(&()); + } else { + cursor.next(&()); + point = cursor.start().0; + } + } + } + } + } + + pub fn text_summary(&self) -> TextSummary { + self.transforms.summary().output.clone() + } + + pub fn text_summary_for_range(&self, range: Range) -> TextSummary { + let mut summary = TextSummary::default(); + + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); + cursor.seek(&range.start, Bias::Right, &()); + + let overshoot = range.start.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let buffer_start = cursor.start().1; + let suffix_start = buffer_start + overshoot; + let suffix_end = + buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0); + summary = self.buffer.text_summary_for_range(suffix_start..suffix_end); + cursor.next(&()); + } + Some(Transform::Inlay(inlay)) => { + let suffix_start = overshoot; + let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0; + summary = inlay.text.cursor(suffix_start).summary(suffix_end); + cursor.next(&()); + } + None => {} + } + + if range.end > cursor.start().0 { + summary += cursor + .summary::<_, TransformSummary>(&range.end, Bias::Right, &()) + .output; + + let overshoot = range.end.0 - cursor.start().0 .0; + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + let prefix_start = cursor.start().1; + let prefix_end = prefix_start + overshoot; + summary += self + .buffer + .text_summary_for_range::(prefix_start..prefix_end); + } + Some(Transform::Inlay(inlay)) => { + let prefix_end = overshoot; + summary += inlay.text.cursor(0).summary::(prefix_end); + } + None => {} + } + } + + summary + } + + pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> { + let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(); + let inlay_point = InlayPoint::new(row, 0); + cursor.seek(&inlay_point, Bias::Left, &()); + + let max_buffer_row = self.buffer.max_point().row; + let mut buffer_point = cursor.start().1; + let buffer_row = if row == 0 { + 0 + } else { + match cursor.item() { + Some(Transform::Isomorphic(_)) => { + buffer_point += inlay_point.0 - cursor.start().0 .0; + buffer_point.row + } + _ => cmp::min(buffer_point.row + 1, max_buffer_row), + } + }; + + InlayBufferRows { + transforms: cursor, + inlay_row: inlay_point.row(), + buffer_rows: self.buffer.buffer_rows(buffer_row), + max_buffer_row, + } + } + + pub fn line_len(&self, row: u32) -> u32 { + let line_start = self.to_offset(InlayPoint::new(row, 0)).0; + let line_end = if row >= self.max_point().row() { + self.len().0 + } else { + self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1 + }; + (line_end - line_start) as u32 + } + + pub fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + text_highlights: Option<&'a TextHighlights>, + hint_highlights: Option, + suggestion_highlights: Option, + ) -> InlayChunks<'a> { + let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); + cursor.seek(&range.start, Bias::Right, &()); + + let mut highlight_endpoints = Vec::new(); + if let Some(text_highlights) = text_highlights { + if !text_highlights.is_empty() { + while cursor.start().0 < range.end { + if true { + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); + + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; + + for (tag, highlights) in text_highlights.iter() { + let style = highlights.0; + let ranges = &highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self + .to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); + } + } + } + + cursor.next(&()); + } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); + } + } + + let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); + let buffer_chunks = self.buffer.chunks(buffer_range, language_aware); + + InlayChunks { + transforms: cursor, + buffer_chunks, + inlay_chunks: None, + buffer_chunk: None, + output_offset: range.start, + max_output_offset: range.end, + hint_highlight_style: hint_highlights, + suggestion_highlight_style: suggestion_highlights, + highlight_endpoints: highlight_endpoints.into_iter().peekable(), + active_highlights: Default::default(), + snapshot: self, + } + } + + #[cfg(test)] + pub fn text(&self) -> String { + self.chunks(Default::default()..self.len(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() + } + + fn check_invariants(&self) { + #[cfg(any(debug_assertions, feature = "test-support"))] + { + assert_eq!(self.transforms.summary().input, self.buffer.text_summary()); + let mut transforms = self.transforms.iter().peekable(); + while let Some(transform) = transforms.next() { + let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_)); + if let Some(next_transform) = transforms.peek() { + let next_transform_is_isomorphic = + matches!(next_transform, Transform::Isomorphic(_)); + assert!( + !transform_is_isomorphic || !next_transform_is_isomorphic, + "two adjacent isomorphic transforms" + ); + } + } + } + } +} + +fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { + if summary.len == 0 { + return; + } + + let mut summary = Some(summary); + sum_tree.update_last( + |transform| { + if let Transform::Isomorphic(transform) = transform { + *transform += summary.take().unwrap(); + } + }, + &(), + ); + + if let Some(summary) = summary { + sum_tree.push(Transform::Isomorphic(summary), &()); + } +} + +#[cfg(test)] +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}; + use sum_tree::TreeMap; + 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); + let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + assert_eq!(inlay_snapshot.text(), "abcdefghi"); + let mut next_inlay_id = 0; + + let (inlay_snapshot, _) = inlay_map.splice( + Vec::new(), + 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!( + inlay_snapshot.to_inlay_point(Point::new(0, 0)), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 1)), + InlayPoint::new(0, 1) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 2)), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 3)), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 4)), + InlayPoint::new(0, 9) + ); + assert_eq!( + inlay_snapshot.to_inlay_point(Point::new(0, 5)), + InlayPoint::new(0, 10) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left), + InlayPoint::new(0, 3) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right), + InlayPoint::new(0, 9) + ); + + // Edits before or after the inlay should not affect it. + buffer.update(cx, |buffer, cx| { + buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx) + }); + let (inlay_snapshot, _) = inlay_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + ); + assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi"); + + // An edit surrounding the inlay should invalidate it. + buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx)); + let (inlay_snapshot, _) = inlay_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + ); + assert_eq!(inlay_snapshot.text(), "abxyDzefghi"); + + let (inlay_snapshot, _) = inlay_map.splice( + Vec::new(), + vec![ + 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"); + + // Edits ending where the inlay starts should not move it if it has a left bias. + buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx)); + let (inlay_snapshot, _) = inlay_map.sync( + buffer.read(cx).snapshot(cx), + buffer_edits.consume().into_inner(), + ); + assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi"); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left), + InlayPoint::new(0, 0) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right), + InlayPoint::new(0, 0) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left), + InlayPoint::new(0, 1) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right), + InlayPoint::new(0, 1) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right), + InlayPoint::new(0, 2) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left), + InlayPoint::new(0, 2) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left), + InlayPoint::new(0, 8) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right), + InlayPoint::new(0, 8) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left), + InlayPoint::new(0, 9) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right), + InlayPoint::new(0, 9) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left), + InlayPoint::new(0, 10) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right), + InlayPoint::new(0, 10) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right), + InlayPoint::new(0, 11) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left), + InlayPoint::new(0, 11) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left), + InlayPoint::new(0, 17) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right), + InlayPoint::new(0, 17) + ); + + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left), + InlayPoint::new(0, 18) + ); + assert_eq!( + inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right), + InlayPoint::new(0, 18) + ); + + // The inlays can be manually removed. + let (inlay_snapshot, _) = inlay_map.splice( + inlay_map.inlays.iter().map(|inlay| inlay.id).collect(), + Vec::new(), + ); + assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi"); + } + + #[gpui::test] + fn test_inlay_buffer_rows(cx: &mut AppContext) { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx)); + assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi"); + let mut next_inlay_id = 0; + + let (inlay_snapshot, _) = inlay_map.splice( + Vec::new(), + vec![ + 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"); + assert_eq!( + inlay_snapshot.buffer_rows(0).collect::>(), + vec![Some(0), None, Some(1), None, None, Some(2)] + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) { + init_test(cx); + + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let len = rng.gen_range(0..30); + let buffer = if rng.gen() { + let text = util::RandomCharIter::new(&mut rng) + .take(len) + .collect::(); + MultiBuffer::build_simple(&text, cx) + } else { + MultiBuffer::build_random(&mut rng, cx) + }; + let mut buffer_snapshot = buffer.read(cx).snapshot(cx); + let mut next_inlay_id = 0; + log::info!("buffer text: {:?}", buffer_snapshot.text()); + + let mut highlights = TreeMap::default(); + let highlight_count = rng.gen_range(0_usize..10); + let mut highlight_ranges = (0..highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting ranges {:?}", highlight_ranges); + let highlight_ranges = highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) + }) + .collect::>(); + + highlights.insert( + Some(TypeId::of::<()>()), + Arc::new((HighlightStyle::default(), highlight_ranges)), + ); + + let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + for _ in 0..operations { + let mut inlay_edits = Patch::default(); + + let mut prev_inlay_text = inlay_snapshot.text(); + let mut buffer_edits = Vec::new(); + match rng.gen_range(0..=100) { + 0..=50 => { + let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + log::info!("mutated text: {:?}", snapshot.text()); + inlay_edits = Patch::new(edits); + } + _ => buffer.update(cx, |buffer, cx| { + let subscription = buffer.subscribe(); + let edit_count = rng.gen_range(1..=5); + buffer.randomly_mutate(&mut rng, edit_count, cx); + buffer_snapshot = buffer.snapshot(cx); + let edits = subscription.consume().into_inner(); + log::info!("editing {:?}", edits); + buffer_edits.extend(edits); + }), + }; + + let (new_inlay_snapshot, new_inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + inlay_snapshot = new_inlay_snapshot; + inlay_edits = inlay_edits.compose(new_inlay_edits); + + log::info!("buffer text: {:?}", buffer_snapshot.text()); + log::info!("inlay text: {:?}", inlay_snapshot.text()); + + let inlays = inlay_map + .inlays + .iter() + .filter(|inlay| inlay.position.is_valid(&buffer_snapshot)) + .map(|inlay| { + let offset = inlay.position.to_offset(&buffer_snapshot); + (offset, inlay.clone()) + }) + .collect::>(); + 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()); + } + assert_eq!(inlay_snapshot.text(), expected_text.to_string()); + + let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::>(); + assert_eq!( + expected_buffer_rows.len() as u32, + expected_text.max_point().row + 1 + ); + for row_start in 0..expected_buffer_rows.len() { + assert_eq!( + inlay_snapshot + .buffer_rows(row_start as u32) + .collect::>(), + &expected_buffer_rows[row_start..], + "incorrect buffer rows starting at {}", + row_start + ); + } + + for _ in 0..5 { + let mut end = rng.gen_range(0..=inlay_snapshot.len().0); + end = expected_text.clip_offset(end, Bias::Right); + let mut start = rng.gen_range(0..=end); + start = expected_text.clip_offset(start, Bias::Right); + + let actual_text = inlay_snapshot + .chunks( + InlayOffset(start)..InlayOffset(end), + false, + Some(&highlights), + None, + None, + ) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!( + actual_text, + expected_text.slice(start..end).to_string(), + "incorrect text in range {:?}", + start..end + ); + + assert_eq!( + inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)), + expected_text.slice(start..end).summary() + ); + } + + for edit in inlay_edits { + prev_inlay_text.replace_range( + edit.new.start.0..edit.new.start.0 + edit.old_len().0, + &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0], + ); + } + assert_eq!(prev_inlay_text, inlay_snapshot.text()); + + assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0); + assert_eq!(expected_text.len(), inlay_snapshot.len().0); + + let mut buffer_point = Point::default(); + let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point); + let mut buffer_chars = buffer_snapshot.chars_at(0); + loop { + // Ensure conversion from buffer coordinates to inlay coordinates + // is consistent. + let buffer_offset = buffer_snapshot.point_to_offset(buffer_point); + assert_eq!( + inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)), + inlay_point + ); + + // No matter which bias we clip an inlay point with, it doesn't move + // because it was constructed from a buffer point. + assert_eq!( + inlay_snapshot.clip_point(inlay_point, Bias::Left), + inlay_point, + "invalid inlay point for buffer point {:?} when clipped left", + buffer_point + ); + assert_eq!( + inlay_snapshot.clip_point(inlay_point, Bias::Right), + inlay_point, + "invalid inlay point for buffer point {:?} when clipped right", + buffer_point + ); + + if let Some(ch) = buffer_chars.next() { + if ch == '\n' { + buffer_point += Point::new(1, 0); + } else { + buffer_point += Point::new(0, ch.len_utf8() as u32); + } + + // Ensure that moving forward in the buffer always moves the inlay point forward as well. + let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point); + assert!(new_inlay_point > inlay_point); + inlay_point = new_inlay_point; + } else { + break; + } + } + + let mut inlay_point = InlayPoint::default(); + let mut inlay_offset = InlayOffset::default(); + for ch in expected_text.chars() { + assert_eq!( + inlay_snapshot.to_offset(inlay_point), + inlay_offset, + "invalid to_offset({:?})", + inlay_point + ); + assert_eq!( + inlay_snapshot.to_point(inlay_offset), + inlay_point, + "invalid to_point({:?})", + inlay_offset + ); + + let mut bytes = [0; 4]; + for byte in ch.encode_utf8(&mut bytes).as_bytes() { + inlay_offset.0 += 1; + if *byte == b'\n' { + inlay_point.0 += Point::new(1, 0); + } else { + inlay_point.0 += Point::new(0, 1); + } + + let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left); + let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right); + assert!( + clipped_left_point <= clipped_right_point, + "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})", + inlay_point, + clipped_left_point, + clipped_right_point + ); + + // Ensure the clipped points are at valid text locations. + assert_eq!( + clipped_left_point.0, + expected_text.clip_point(clipped_left_point.0, Bias::Left) + ); + assert_eq!( + clipped_right_point.0, + expected_text.clip_point(clipped_right_point.0, Bias::Right) + ); + + // Ensure the clipped points never overshoot the end of the map. + assert!(clipped_left_point <= inlay_snapshot.max_point()); + assert!(clipped_right_point <= inlay_snapshot.max_point()); + + // Ensure the clipped points are at valid buffer locations. + assert_eq!( + inlay_snapshot + .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)), + clipped_left_point, + "to_buffer_point({:?}) = {:?}", + clipped_left_point, + inlay_snapshot.to_buffer_point(clipped_left_point), + ); + assert_eq!( + inlay_snapshot + .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)), + clipped_right_point, + "to_buffer_point({:?}) = {:?}", + clipped_right_point, + inlay_snapshot.to_buffer_point(clipped_right_point), + ); + } + } + } + } + + fn init_test(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + } +} diff --git a/crates/editor/src/display_map/suggestion_map.rs b/crates/editor/src/display_map/suggestion_map.rs deleted file mode 100644 index eac903d0af..0000000000 --- a/crates/editor/src/display_map/suggestion_map.rs +++ /dev/null @@ -1,871 +0,0 @@ -use super::{ - fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot}, - TextHighlights, -}; -use crate::{MultiBufferSnapshot, ToPoint}; -use gpui::fonts::HighlightStyle; -use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary}; -use parking_lot::Mutex; -use std::{ - cmp, - ops::{Add, AddAssign, Range, Sub}, -}; -use util::post_inc; - -pub type SuggestionEdit = Edit; - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct SuggestionOffset(pub usize); - -impl Add for SuggestionOffset { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - Self(self.0 + rhs.0) - } -} - -impl Sub for SuggestionOffset { - type Output = Self; - - fn sub(self, rhs: Self) -> Self::Output { - Self(self.0 - rhs.0) - } -} - -impl AddAssign for SuggestionOffset { - fn add_assign(&mut self, rhs: Self) { - self.0 += rhs.0; - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct SuggestionPoint(pub Point); - -impl SuggestionPoint { - pub fn new(row: u32, column: u32) -> Self { - Self(Point::new(row, column)) - } - - pub fn row(self) -> u32 { - self.0.row - } - - pub fn column(self) -> u32 { - self.0.column - } -} - -#[derive(Clone, Debug)] -pub struct Suggestion { - pub position: T, - pub text: Rope, -} - -pub struct SuggestionMap(Mutex); - -impl SuggestionMap { - pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) { - let snapshot = SuggestionSnapshot { - fold_snapshot, - suggestion: None, - version: 0, - }; - (Self(Mutex::new(snapshot.clone())), snapshot) - } - - pub fn replace( - &self, - new_suggestion: Option>, - fold_snapshot: FoldSnapshot, - fold_edits: Vec, - ) -> ( - SuggestionSnapshot, - Vec, - Option>, - ) - where - T: ToPoint, - { - let new_suggestion = new_suggestion.map(|new_suggestion| { - let buffer_point = new_suggestion - .position - .to_point(fold_snapshot.buffer_snapshot()); - let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left); - let fold_offset = fold_point.to_offset(&fold_snapshot); - Suggestion { - position: fold_offset, - text: new_suggestion.text, - } - }); - - let (_, edits) = self.sync(fold_snapshot, fold_edits); - let mut snapshot = self.0.lock(); - - let mut patch = Patch::new(edits); - let old_suggestion = snapshot.suggestion.take(); - if let Some(suggestion) = &old_suggestion { - patch = patch.compose([SuggestionEdit { - old: SuggestionOffset(suggestion.position.0) - ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()), - new: SuggestionOffset(suggestion.position.0) - ..SuggestionOffset(suggestion.position.0), - }]); - } - - if let Some(suggestion) = new_suggestion.as_ref() { - patch = patch.compose([SuggestionEdit { - old: SuggestionOffset(suggestion.position.0) - ..SuggestionOffset(suggestion.position.0), - new: SuggestionOffset(suggestion.position.0) - ..SuggestionOffset(suggestion.position.0 + suggestion.text.len()), - }]); - } - - snapshot.suggestion = new_suggestion; - snapshot.version += 1; - (snapshot.clone(), patch.into_inner(), old_suggestion) - } - - pub fn sync( - &self, - fold_snapshot: FoldSnapshot, - fold_edits: Vec, - ) -> (SuggestionSnapshot, Vec) { - let mut snapshot = self.0.lock(); - - if snapshot.fold_snapshot.version != fold_snapshot.version { - snapshot.version += 1; - } - - let mut suggestion_edits = Vec::new(); - - let mut suggestion_old_len = 0; - let mut suggestion_new_len = 0; - for fold_edit in fold_edits { - let start = fold_edit.new.start; - let end = FoldOffset(start.0 + fold_edit.old_len().0); - if let Some(suggestion) = snapshot.suggestion.as_mut() { - if end <= suggestion.position { - suggestion.position.0 += fold_edit.new_len().0; - suggestion.position.0 -= fold_edit.old_len().0; - } else if start > suggestion.position { - suggestion_old_len = suggestion.text.len(); - suggestion_new_len = suggestion_old_len; - } else { - suggestion_old_len = suggestion.text.len(); - snapshot.suggestion.take(); - suggestion_edits.push(SuggestionEdit { - old: SuggestionOffset(fold_edit.old.start.0) - ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len), - new: SuggestionOffset(fold_edit.new.start.0) - ..SuggestionOffset(fold_edit.new.end.0), - }); - continue; - } - } - - suggestion_edits.push(SuggestionEdit { - old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len) - ..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len), - new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len) - ..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len), - }); - } - snapshot.fold_snapshot = fold_snapshot; - - (snapshot.clone(), suggestion_edits) - } - - pub fn has_suggestion(&self) -> bool { - let snapshot = self.0.lock(); - snapshot.suggestion.is_some() - } -} - -#[derive(Clone)] -pub struct SuggestionSnapshot { - pub fold_snapshot: FoldSnapshot, - pub suggestion: Option>, - pub version: usize, -} - -impl SuggestionSnapshot { - pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { - self.fold_snapshot.buffer_snapshot() - } - - pub fn max_point(&self) -> SuggestionPoint { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_point = suggestion.position.to_point(&self.fold_snapshot); - let mut max_point = suggestion_point.0; - max_point += suggestion.text.max_point(); - max_point += self.fold_snapshot.max_point().0 - suggestion_point.0; - SuggestionPoint(max_point) - } else { - SuggestionPoint(self.fold_snapshot.max_point().0) - } - } - - pub fn len(&self) -> SuggestionOffset { - if let Some(suggestion) = self.suggestion.as_ref() { - let mut len = suggestion.position.0; - len += suggestion.text.len(); - len += self.fold_snapshot.len().0 - suggestion.position.0; - SuggestionOffset(len) - } else { - SuggestionOffset(self.fold_snapshot.len().0) - } - } - - pub fn line_len(&self, row: u32) -> u32 { - if let Some(suggestion) = &self.suggestion { - let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; - let suggestion_end = suggestion_start + suggestion.text.max_point(); - - if row < suggestion_start.row { - self.fold_snapshot.line_len(row) - } else if row > suggestion_end.row { - self.fold_snapshot - .line_len(suggestion_start.row + (row - suggestion_end.row)) - } else { - let mut result = suggestion.text.line_len(row - suggestion_start.row); - if row == suggestion_start.row { - result += suggestion_start.column; - } - if row == suggestion_end.row { - result += - self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column; - } - result - } - } else { - self.fold_snapshot.line_len(row) - } - } - - pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; - let suggestion_end = suggestion_start + suggestion.text.max_point(); - if point.0 <= suggestion_start { - SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0) - } else if point.0 > suggestion_end { - let fold_point = self.fold_snapshot.clip_point( - FoldPoint(suggestion_start + (point.0 - suggestion_end)), - bias, - ); - let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start); - if bias == Bias::Left && suggestion_point == suggestion_end { - SuggestionPoint(suggestion_start) - } else { - SuggestionPoint(suggestion_point) - } - } else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 { - SuggestionPoint(suggestion_start) - } else { - let fold_point = if self.fold_snapshot.line_len(suggestion_start.row) - > suggestion_start.column - { - FoldPoint(suggestion_start + Point::new(0, 1)) - } else { - FoldPoint(suggestion_start + Point::new(1, 0)) - }; - let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias); - SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start)) - } - } else { - SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0) - } - } - - pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; - let suggestion_end = suggestion_start + suggestion.text.max_point(); - - if point.0 <= suggestion_start { - SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0) - } else if point.0 > suggestion_end { - let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end)) - .to_offset(&self.fold_snapshot); - SuggestionOffset(fold_offset.0 + suggestion.text.len()) - } else { - let offset_in_suggestion = - suggestion.text.point_to_offset(point.0 - suggestion_start); - SuggestionOffset(suggestion.position.0 + offset_in_suggestion) - } - } else { - SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0) - } - } - - pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0; - if offset.0 <= suggestion.position.0 { - SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0) - } else if offset.0 > (suggestion.position.0 + suggestion.text.len()) { - let fold_point = FoldOffset(offset.0 - suggestion.text.len()) - .to_point(&self.fold_snapshot) - .0; - - SuggestionPoint( - suggestion_point_start - + suggestion.text.max_point() - + (fold_point - suggestion_point_start), - ) - } else { - let point_in_suggestion = suggestion - .text - .offset_to_point(offset.0 - suggestion.position.0); - SuggestionPoint(suggestion_point_start + point_in_suggestion) - } - } else { - SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0) - } - } - - pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; - let suggestion_end = suggestion_start + suggestion.text.max_point(); - - if point.0 <= suggestion_start { - FoldPoint(point.0) - } else if point.0 > suggestion_end { - FoldPoint(suggestion_start + (point.0 - suggestion_end)) - } else { - FoldPoint(suggestion_start) - } - } else { - FoldPoint(point.0) - } - } - - pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; - - if point.0 <= suggestion_start { - SuggestionPoint(point.0) - } else { - let suggestion_end = suggestion_start + suggestion.text.max_point(); - SuggestionPoint(suggestion_end + (point.0 - suggestion_start)) - } - } else { - SuggestionPoint(point.0) - } - } - - pub fn text_summary_for_range(&self, range: Range) -> TextSummary { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0; - let suggestion_end = suggestion_start + suggestion.text.max_point(); - let mut summary = TextSummary::default(); - - let prefix_range = - cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start); - if prefix_range.start < prefix_range.end { - summary += self.fold_snapshot.text_summary_for_range( - FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end), - ); - } - - let suggestion_range = - cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end); - if suggestion_range.start < suggestion_range.end { - let point_range = suggestion_range.start - suggestion_start - ..suggestion_range.end - suggestion_start; - let offset_range = suggestion.text.point_to_offset(point_range.start) - ..suggestion.text.point_to_offset(point_range.end); - summary += suggestion - .text - .cursor(offset_range.start) - .summary::(offset_range.end); - } - - let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0; - if suffix_range.start < suffix_range.end { - let start = suggestion_start + (suffix_range.start - suggestion_end); - let end = suggestion_start + (suffix_range.end - suggestion_end); - summary += self - .fold_snapshot - .text_summary_for_range(FoldPoint(start)..FoldPoint(end)); - } - - summary - } else { - self.fold_snapshot - .text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0)) - } - } - - pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator { - let start = self.to_offset(start); - self.chunks(start..self.len(), false, None, None) - .flat_map(|chunk| chunk.text.chars()) - } - - pub fn chunks<'a>( - &'a self, - range: Range, - language_aware: bool, - text_highlights: Option<&'a TextHighlights>, - suggestion_highlight: Option, - ) -> SuggestionChunks<'a> { - if let Some(suggestion) = self.suggestion.as_ref() { - let suggestion_range = - suggestion.position.0..suggestion.position.0 + suggestion.text.len(); - - let prefix_chunks = if range.start.0 < suggestion_range.start { - Some(self.fold_snapshot.chunks( - FoldOffset(range.start.0) - ..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)), - language_aware, - text_highlights, - )) - } else { - None - }; - - let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start) - ..cmp::min(range.end.0, suggestion_range.end); - let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end - { - let start = clipped_suggestion_range.start - suggestion_range.start; - let end = clipped_suggestion_range.end - suggestion_range.start; - Some(suggestion.text.chunks_in_range(start..end)) - } else { - None - }; - - let suffix_chunks = if range.end.0 > suggestion_range.end { - let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len(); - let end = range.end.0 - suggestion_range.len(); - Some(self.fold_snapshot.chunks( - FoldOffset(start)..FoldOffset(end), - language_aware, - text_highlights, - )) - } else { - None - }; - - SuggestionChunks { - prefix_chunks, - suggestion_chunks, - suffix_chunks, - highlight_style: suggestion_highlight, - } - } else { - SuggestionChunks { - prefix_chunks: Some(self.fold_snapshot.chunks( - FoldOffset(range.start.0)..FoldOffset(range.end.0), - language_aware, - text_highlights, - )), - suggestion_chunks: None, - suffix_chunks: None, - highlight_style: None, - } - } - } - - pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> { - let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() { - let start = suggestion.position.to_point(&self.fold_snapshot).0; - let end = start + suggestion.text.max_point(); - start.row..end.row - } else { - u32::MAX..u32::MAX - }; - - let fold_buffer_rows = if row <= suggestion_range.start { - self.fold_snapshot.buffer_rows(row) - } else if row > suggestion_range.end { - self.fold_snapshot - .buffer_rows(row - (suggestion_range.end - suggestion_range.start)) - } else { - let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start); - rows.next(); - rows - }; - - SuggestionBufferRows { - current_row: row, - suggestion_row_start: suggestion_range.start, - suggestion_row_end: suggestion_range.end, - fold_buffer_rows, - } - } - - #[cfg(test)] - pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None, None) - .map(|chunk| chunk.text) - .collect() - } -} - -pub struct SuggestionChunks<'a> { - prefix_chunks: Option>, - suggestion_chunks: Option>, - suffix_chunks: Option>, - highlight_style: Option, -} - -impl<'a> Iterator for SuggestionChunks<'a> { - type Item = Chunk<'a>; - - fn next(&mut self) -> Option { - if let Some(chunks) = self.prefix_chunks.as_mut() { - if let Some(chunk) = chunks.next() { - return Some(chunk); - } else { - self.prefix_chunks = None; - } - } - - if let Some(chunks) = self.suggestion_chunks.as_mut() { - if let Some(chunk) = chunks.next() { - return Some(Chunk { - text: chunk, - highlight_style: self.highlight_style, - ..Default::default() - }); - } else { - self.suggestion_chunks = None; - } - } - - if let Some(chunks) = self.suffix_chunks.as_mut() { - if let Some(chunk) = chunks.next() { - return Some(chunk); - } else { - self.suffix_chunks = None; - } - } - - None - } -} - -#[derive(Clone)] -pub struct SuggestionBufferRows<'a> { - current_row: u32, - suggestion_row_start: u32, - suggestion_row_end: u32, - fold_buffer_rows: FoldBufferRows<'a>, -} - -impl<'a> Iterator for SuggestionBufferRows<'a> { - type Item = Option; - - fn next(&mut self) -> Option { - let row = post_inc(&mut self.current_row); - if row <= self.suggestion_row_start || row > self.suggestion_row_end { - self.fold_buffer_rows.next() - } else { - Some(None) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{display_map::fold_map::FoldMap, MultiBuffer}; - use gpui::AppContext; - use rand::{prelude::StdRng, Rng}; - use settings::SettingsStore; - use std::{ - env, - ops::{Bound, RangeBounds}, - }; - - #[gpui::test] - fn test_basic(cx: &mut AppContext) { - let buffer = MultiBuffer::build_simple("abcdefghi", cx); - let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe()); - let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx)); - let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone()); - assert_eq!(suggestion_snapshot.text(), "abcdefghi"); - - let (suggestion_snapshot, _, _) = suggestion_map.replace( - Some(Suggestion { - position: 3, - text: "123\n456".into(), - }), - fold_snapshot, - Default::default(), - ); - assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi"); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")], - None, - cx, - ) - }); - let (fold_snapshot, fold_edits) = fold_map.read( - buffer.read(cx).snapshot(cx), - buffer_edits.consume().into_inner(), - ); - let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits); - assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL"); - - let (mut fold_map_writer, _, _) = - fold_map.write(buffer.read(cx).snapshot(cx), Default::default()); - let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]); - let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits); - assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL"); - - let (mut fold_map_writer, _, _) = - fold_map.write(buffer.read(cx).snapshot(cx), Default::default()); - let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]); - let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits); - assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL"); - } - - #[gpui::test(iterations = 100)] - fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) { - init_test(cx); - - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let len = rng.gen_range(0..30); - let buffer = if rng.gen() { - let text = util::RandomCharIter::new(&mut rng) - .take(len) - .collect::(); - MultiBuffer::build_simple(&text, cx) - } else { - MultiBuffer::build_random(&mut rng, cx) - }; - let mut buffer_snapshot = buffer.read(cx).snapshot(cx); - log::info!("buffer text: {:?}", buffer_snapshot.text()); - - let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone()); - - for _ in 0..operations { - let mut suggestion_edits = Patch::default(); - - let mut prev_suggestion_text = suggestion_snapshot.text(); - let mut buffer_edits = Vec::new(); - match rng.gen_range(0..=100) { - 0..=29 => { - let (_, edits) = suggestion_map.randomly_mutate(&mut rng); - suggestion_edits = suggestion_edits.compose(edits); - } - 30..=59 => { - for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { - fold_snapshot = new_fold_snapshot; - let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits); - suggestion_edits = suggestion_edits.compose(edits); - } - } - _ => buffer.update(cx, |buffer, cx| { - let subscription = buffer.subscribe(); - let edit_count = rng.gen_range(1..=5); - buffer.randomly_mutate(&mut rng, edit_count, cx); - buffer_snapshot = buffer.snapshot(cx); - let edits = subscription.consume().into_inner(); - log::info!("editing {:?}", edits); - buffer_edits.extend(edits); - }), - }; - - let (new_fold_snapshot, fold_edits) = - fold_map.read(buffer_snapshot.clone(), buffer_edits); - fold_snapshot = new_fold_snapshot; - let (new_suggestion_snapshot, edits) = - suggestion_map.sync(fold_snapshot.clone(), fold_edits); - suggestion_snapshot = new_suggestion_snapshot; - suggestion_edits = suggestion_edits.compose(edits); - - log::info!("buffer text: {:?}", buffer_snapshot.text()); - log::info!("folds text: {:?}", fold_snapshot.text()); - log::info!("suggestions text: {:?}", suggestion_snapshot.text()); - - let mut expected_text = Rope::from(fold_snapshot.text().as_str()); - let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::>(); - if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() { - expected_text.replace( - suggestion.position.0..suggestion.position.0, - &suggestion.text.to_string(), - ); - let suggestion_start = suggestion.position.to_point(&fold_snapshot).0; - let suggestion_end = suggestion_start + suggestion.text.max_point(); - expected_buffer_rows.splice( - (suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize, - (0..suggestion_end.row - suggestion_start.row).map(|_| None), - ); - } - assert_eq!(suggestion_snapshot.text(), expected_text.to_string()); - for row_start in 0..expected_buffer_rows.len() { - assert_eq!( - suggestion_snapshot - .buffer_rows(row_start as u32) - .collect::>(), - &expected_buffer_rows[row_start..], - "incorrect buffer rows starting at {}", - row_start - ); - } - - for _ in 0..5 { - let mut end = rng.gen_range(0..=suggestion_snapshot.len().0); - end = expected_text.clip_offset(end, Bias::Right); - let mut start = rng.gen_range(0..=end); - start = expected_text.clip_offset(start, Bias::Right); - - let actual_text = suggestion_snapshot - .chunks( - SuggestionOffset(start)..SuggestionOffset(end), - false, - None, - None, - ) - .map(|chunk| chunk.text) - .collect::(); - assert_eq!( - actual_text, - expected_text.slice(start..end).to_string(), - "incorrect text in range {:?}", - start..end - ); - - let start_point = SuggestionPoint(expected_text.offset_to_point(start)); - let end_point = SuggestionPoint(expected_text.offset_to_point(end)); - assert_eq!( - suggestion_snapshot.text_summary_for_range(start_point..end_point), - expected_text.slice(start..end).summary() - ); - } - - for edit in suggestion_edits.into_inner() { - prev_suggestion_text.replace_range( - edit.new.start.0..edit.new.start.0 + edit.old_len().0, - &suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0], - ); - } - assert_eq!(prev_suggestion_text, suggestion_snapshot.text()); - - assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0); - assert_eq!(expected_text.len(), suggestion_snapshot.len().0); - - let mut suggestion_point = SuggestionPoint::default(); - let mut suggestion_offset = SuggestionOffset::default(); - for ch in expected_text.chars() { - assert_eq!( - suggestion_snapshot.to_offset(suggestion_point), - suggestion_offset, - "invalid to_offset({:?})", - suggestion_point - ); - assert_eq!( - suggestion_snapshot.to_point(suggestion_offset), - suggestion_point, - "invalid to_point({:?})", - suggestion_offset - ); - assert_eq!( - suggestion_snapshot - .to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)), - suggestion_snapshot.clip_point(suggestion_point, Bias::Left), - ); - - let mut bytes = [0; 4]; - for byte in ch.encode_utf8(&mut bytes).as_bytes() { - suggestion_offset.0 += 1; - if *byte == b'\n' { - suggestion_point.0 += Point::new(1, 0); - } else { - suggestion_point.0 += Point::new(0, 1); - } - - let clipped_left_point = - suggestion_snapshot.clip_point(suggestion_point, Bias::Left); - let clipped_right_point = - suggestion_snapshot.clip_point(suggestion_point, Bias::Right); - assert!( - clipped_left_point <= clipped_right_point, - "clipped left point {:?} is greater than clipped right point {:?}", - clipped_left_point, - clipped_right_point - ); - assert_eq!( - clipped_left_point.0, - expected_text.clip_point(clipped_left_point.0, Bias::Left) - ); - assert_eq!( - clipped_right_point.0, - expected_text.clip_point(clipped_right_point.0, Bias::Right) - ); - assert!(clipped_left_point <= suggestion_snapshot.max_point()); - assert!(clipped_right_point <= suggestion_snapshot.max_point()); - - if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() { - let suggestion_start = suggestion.position.to_point(&fold_snapshot).0; - let suggestion_end = suggestion_start + suggestion.text.max_point(); - let invalid_range = ( - Bound::Excluded(suggestion_start), - Bound::Included(suggestion_end), - ); - assert!( - !invalid_range.contains(&clipped_left_point.0), - "clipped left point {:?} is inside invalid suggestion range {:?}", - clipped_left_point, - invalid_range - ); - assert!( - !invalid_range.contains(&clipped_right_point.0), - "clipped right point {:?} is inside invalid suggestion range {:?}", - clipped_right_point, - invalid_range - ); - } - } - } - } - } - - fn init_test(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); - theme::init((), cx); - } - - impl SuggestionMap { - pub fn randomly_mutate( - &self, - rng: &mut impl Rng, - ) -> (SuggestionSnapshot, Vec) { - let fold_snapshot = self.0.lock().fold_snapshot.clone(); - let new_suggestion = if rng.gen_bool(0.3) { - None - } else { - let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len()); - let len = rng.gen_range(0..30); - Some(Suggestion { - position: index, - text: util::RandomCharIter::new(rng) - .take(len) - .filter(|ch| *ch != '\r') - .collect::() - .as_str() - .into(), - }) - }; - - log::info!("replacing suggestion with {:?}", new_suggestion); - let (snapshot, edits, _) = - self.replace(new_suggestion, fold_snapshot, Default::default()); - (snapshot, edits) - } - } -} diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index d97ba4f40b..ca73f6a1a7 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,80 +1,76 @@ use super::{ - suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot}, + fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; use language::{Chunk, Point}; -use parking_lot::Mutex; use std::{cmp, mem, num::NonZeroU32, ops::Range}; use sum_tree::Bias; const MAX_EXPANSION_COLUMN: u32 = 256; -pub struct TabMap(Mutex); +pub struct TabMap(TabSnapshot); impl TabMap { - pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) { + pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) { let snapshot = TabSnapshot { - suggestion_snapshot: input, + fold_snapshot, tab_size, max_expansion_column: MAX_EXPANSION_COLUMN, version: 0, }; - (Self(Mutex::new(snapshot.clone())), snapshot) + (Self(snapshot.clone()), snapshot) } #[cfg(test)] - pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot { - self.0.lock().max_expansion_column = column; - self.0.lock().clone() + pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot { + self.0.max_expansion_column = column; + self.0.clone() } pub fn sync( - &self, - suggestion_snapshot: SuggestionSnapshot, - mut suggestion_edits: Vec, + &mut self, + fold_snapshot: FoldSnapshot, + mut fold_edits: Vec, tab_size: NonZeroU32, ) -> (TabSnapshot, Vec) { - let mut old_snapshot = self.0.lock(); + let old_snapshot = &mut self.0; let mut new_snapshot = TabSnapshot { - suggestion_snapshot, + fold_snapshot, tab_size, max_expansion_column: old_snapshot.max_expansion_column, version: old_snapshot.version, }; - if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version { + if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version { new_snapshot.version += 1; } - let mut tab_edits = Vec::with_capacity(suggestion_edits.len()); + let mut tab_edits = Vec::with_capacity(fold_edits.len()); if old_snapshot.tab_size == new_snapshot.tab_size { // Expand each edit to include the next tab on the same line as the edit, // and any subsequent tabs on that line that moved across the tab expansion // boundary. - for suggestion_edit in &mut suggestion_edits { - let old_end = old_snapshot - .suggestion_snapshot - .to_point(suggestion_edit.old.end); - let old_end_row_successor_offset = - old_snapshot.suggestion_snapshot.to_offset(cmp::min( - SuggestionPoint::new(old_end.row() + 1, 0), - old_snapshot.suggestion_snapshot.max_point(), - )); - let new_end = new_snapshot - .suggestion_snapshot - .to_point(suggestion_edit.new.end); + for fold_edit in &mut fold_edits { + let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); + let old_end_row_successor_offset = cmp::min( + FoldPoint::new(old_end.row() + 1, 0), + old_snapshot.fold_snapshot.max_point(), + ) + .to_offset(&old_snapshot.fold_snapshot); + let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot); let mut offset_from_edit = 0; let mut first_tab_offset = None; let mut last_tab_with_changed_expansion_offset = None; - 'outer: for chunk in old_snapshot.suggestion_snapshot.chunks( - suggestion_edit.old.end..old_end_row_successor_offset, + 'outer: for chunk in old_snapshot.fold_snapshot.chunks( + fold_edit.old.end..old_end_row_successor_offset, false, None, None, + None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -102,39 +98,31 @@ impl TabMap { } if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) { - suggestion_edit.old.end.0 += offset as usize + 1; - suggestion_edit.new.end.0 += offset as usize + 1; + fold_edit.old.end.0 += offset as usize + 1; + fold_edit.new.end.0 += offset as usize + 1; } } // Combine any edits that overlap due to the expansion. let mut ix = 1; - while ix < suggestion_edits.len() { - let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix); + while ix < fold_edits.len() { + let (prev_edits, next_edits) = fold_edits.split_at_mut(ix); let prev_edit = prev_edits.last_mut().unwrap(); let edit = &next_edits[0]; if prev_edit.old.end >= edit.old.start { prev_edit.old.end = edit.old.end; prev_edit.new.end = edit.new.end; - suggestion_edits.remove(ix); + fold_edits.remove(ix); } else { ix += 1; } } - for suggestion_edit in suggestion_edits { - let old_start = old_snapshot - .suggestion_snapshot - .to_point(suggestion_edit.old.start); - let old_end = old_snapshot - .suggestion_snapshot - .to_point(suggestion_edit.old.end); - let new_start = new_snapshot - .suggestion_snapshot - .to_point(suggestion_edit.new.start); - let new_end = new_snapshot - .suggestion_snapshot - .to_point(suggestion_edit.new.end); + for fold_edit in fold_edits { + let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot); + let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot); + let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot); + let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot); tab_edits.push(TabEdit { old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end), new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end), @@ -155,7 +143,7 @@ impl TabMap { #[derive(Clone)] pub struct TabSnapshot { - pub suggestion_snapshot: SuggestionSnapshot, + pub fold_snapshot: FoldSnapshot, pub tab_size: NonZeroU32, pub max_expansion_column: u32, pub version: usize, @@ -163,18 +151,15 @@ pub struct TabSnapshot { impl TabSnapshot { pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot { - self.suggestion_snapshot.buffer_snapshot() + &self.fold_snapshot.inlay_snapshot.buffer } pub fn line_len(&self, row: u32) -> u32 { let max_point = self.max_point(); if row < max_point.row() { - self.to_tab_point(SuggestionPoint::new( - row, - self.suggestion_snapshot.line_len(row), - )) - .0 - .column + self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row))) + .0 + .column } else { max_point.column() } @@ -185,10 +170,10 @@ impl TabSnapshot { } pub fn text_summary_for_range(&self, range: Range) -> TextSummary { - let input_start = self.to_suggestion_point(range.start, Bias::Left).0; - let input_end = self.to_suggestion_point(range.end, Bias::Right).0; + let input_start = self.to_fold_point(range.start, Bias::Left).0; + let input_end = self.to_fold_point(range.end, Bias::Right).0; let input_summary = self - .suggestion_snapshot + .fold_snapshot .text_summary_for_range(input_start..input_end); let mut first_line_chars = 0; @@ -198,7 +183,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None) + .chunks(range.start..line_end, false, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -217,6 +202,7 @@ impl TabSnapshot { false, None, None, + None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -238,15 +224,17 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - suggestion_highlight: Option, + hint_highlights: Option, + suggestion_highlights: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = - self.to_suggestion_point(range.start, Bias::Left); + self.to_fold_point(range.start, Bias::Left); let input_column = input_start.column(); - let input_start = self.suggestion_snapshot.to_offset(input_start); + let input_start = input_start.to_offset(&self.fold_snapshot); let input_end = self - .suggestion_snapshot - .to_offset(self.to_suggestion_point(range.end, Bias::Right).0); + .to_fold_point(range.end, Bias::Right) + .0 + .to_offset(&self.fold_snapshot); let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 { range.end.column() - range.start.column() } else { @@ -254,11 +242,12 @@ impl TabSnapshot { }; TabChunks { - suggestion_chunks: self.suggestion_snapshot.chunks( + fold_chunks: self.fold_snapshot.chunks( input_start..input_end, language_aware, text_highlights, - suggestion_highlight, + hint_highlights, + suggestion_highlights, ), input_column, column: expanded_char_column, @@ -275,63 +264,58 @@ impl TabSnapshot { } } - pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows { - self.suggestion_snapshot.buffer_rows(row) + pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> { + self.fold_snapshot.buffer_rows(row) } #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None, None) + self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) .map(|chunk| chunk.text) .collect() } pub fn max_point(&self) -> TabPoint { - self.to_tab_point(self.suggestion_snapshot.max_point()) + self.to_tab_point(self.fold_snapshot.max_point()) } pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint { self.to_tab_point( - self.suggestion_snapshot - .clip_point(self.to_suggestion_point(point, bias).0, bias), + self.fold_snapshot + .clip_point(self.to_fold_point(point, bias).0, bias), ) } - pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint { - let chars = self - .suggestion_snapshot - .chars_at(SuggestionPoint::new(input.row(), 0)); + pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint { + let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0)); let expanded = self.expand_tabs(chars, input.column()); TabPoint::new(input.row(), expanded) } - pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) { - let chars = self - .suggestion_snapshot - .chars_at(SuggestionPoint::new(output.row(), 0)); + pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) { + let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0)); let expanded = output.column(); let (collapsed, expanded_char_column, to_next_stop) = self.collapse_tabs(chars, expanded, bias); ( - SuggestionPoint::new(output.row(), collapsed as u32), + FoldPoint::new(output.row(), collapsed as u32), expanded_char_column, to_next_stop, ) } pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint { - let fold_point = self - .suggestion_snapshot - .fold_snapshot - .to_fold_point(point, bias); - let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point); - self.to_tab_point(suggestion_point) + let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + self.to_tab_point(fold_point) } pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point { - let suggestion_point = self.to_suggestion_point(point, bias).0; - let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point); - fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot) + let fold_point = self.to_fold_point(point, bias).0; + let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); + self.fold_snapshot + .inlay_snapshot + .to_buffer_point(inlay_point) } fn expand_tabs(&self, chars: impl Iterator, column: u32) -> u32 { @@ -490,7 +474,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary { const SPACES: &str = " "; pub struct TabChunks<'a> { - suggestion_chunks: SuggestionChunks<'a>, + fold_chunks: FoldChunks<'a>, chunk: Chunk<'a>, column: u32, max_expansion_column: u32, @@ -506,7 +490,7 @@ impl<'a> Iterator for TabChunks<'a> { fn next(&mut self) -> Option { if self.chunk.text.is_empty() { - if let Some(chunk) = self.suggestion_chunks.next() { + if let Some(chunk) = self.fold_chunks.next() { self.chunk = chunk; if self.inside_leading_tab { self.chunk.text = &self.chunk.text[1..]; @@ -574,7 +558,7 @@ impl<'a> Iterator for TabChunks<'a> { mod tests { use super::*; use crate::{ - display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap}, + display_map::{fold_map::FoldMap, inlay_map::InlayMap}, MultiBuffer, }; use rand::{prelude::StdRng, Rng}; @@ -583,9 +567,9 @@ mod tests { fn test_expand_tabs(cx: &mut gpui::AppContext) { let buffer = MultiBuffer::build_simple("", cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0); assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4); @@ -600,9 +584,9 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); tab_snapshot.max_expansion_column = max_expansion_column; assert_eq!(tab_snapshot.text(), output); @@ -615,6 +599,7 @@ mod tests { false, None, None, + None, ) .map(|c| c.text) .collect::(), @@ -626,16 +611,16 @@ mod tests { let input_point = Point::new(0, ix as u32); let output_point = Point::new(0, output.find(c).unwrap() as u32); assert_eq!( - tab_snapshot.to_tab_point(SuggestionPoint(input_point)), + tab_snapshot.to_tab_point(FoldPoint(input_point)), TabPoint(output_point), "to_tab_point({input_point:?})" ); assert_eq!( tab_snapshot - .to_suggestion_point(TabPoint(output_point), Bias::Left) + .to_fold_point(TabPoint(output_point), Bias::Left) .0, - SuggestionPoint(input_point), - "to_suggestion_point({output_point:?})" + FoldPoint(input_point), + "to_fold_point({output_point:?})" ); } } @@ -648,9 +633,9 @@ mod tests { let buffer = MultiBuffer::build_simple(input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); tab_snapshot.max_expansion_column = max_expansion_column; assert_eq!(tab_snapshot.text(), input); @@ -662,9 +647,9 @@ mod tests { let buffer = MultiBuffer::build_simple(&input, cx); let buffer_snapshot = buffer.read(cx).snapshot(cx); - let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); - let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot); - let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap()); + let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); assert_eq!( chunks(&tab_snapshot, TabPoint::zero()), @@ -689,7 +674,7 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -721,15 +706,16 @@ mod tests { let buffer_snapshot = buffer.read(cx).snapshot(cx); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone()); fold_map.randomly_mutate(&mut rng); - let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]); + let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]); log::info!("FoldMap text: {:?}", fold_snapshot.text()); - let (suggestion_map, _) = SuggestionMap::new(fold_snapshot); - let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng); - log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text()); + let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); - let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); let text = text::Rope::from(tabs_snapshot.text().as_str()); @@ -757,7 +743,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), expected_text, @@ -767,7 +753,7 @@ mod tests { ); let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end); - if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') { + if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') { actual_summary.longest_row = expected_summary.longest_row; actual_summary.longest_row_chars = expected_summary.longest_row_chars; } diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 478eaf4c7e..f21c7151ad 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,5 +1,5 @@ use super::{ - suggestion_map::SuggestionBufferRows, + fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, TextHighlights, }; @@ -65,7 +65,7 @@ pub struct WrapChunks<'a> { #[derive(Clone)] pub struct WrapBufferRows<'a> { - input_buffer_rows: SuggestionBufferRows<'a>, + input_buffer_rows: FoldBufferRows<'a>, input_buffer_row: Option, output_row: u32, soft_wrapped: bool, @@ -353,7 +353,7 @@ impl WrapSnapshot { } old_cursor.next(&()); - new_transforms.push_tree( + new_transforms.append( old_cursor.slice(&next_edit.old.start, Bias::Right, &()), &(), ); @@ -366,7 +366,7 @@ impl WrapSnapshot { new_transforms.push_or_extend(Transform::isomorphic(summary)); } old_cursor.next(&()); - new_transforms.push_tree(old_cursor.suffix(&()), &()); + new_transforms.append(old_cursor.suffix(&()), &()); } } } @@ -446,6 +446,7 @@ impl WrapSnapshot { false, None, None, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -500,7 +501,7 @@ impl WrapSnapshot { new_transforms.push_or_extend(Transform::isomorphic(summary)); } old_cursor.next(&()); - new_transforms.push_tree( + new_transforms.append( old_cursor.slice( &TabPoint::new(next_edit.old_rows.start, 0), Bias::Right, @@ -517,7 +518,7 @@ impl WrapSnapshot { new_transforms.push_or_extend(Transform::isomorphic(summary)); } old_cursor.next(&()); - new_transforms.push_tree(old_cursor.suffix(&()), &()); + new_transforms.append(old_cursor.suffix(&()), &()); } } } @@ -575,7 +576,8 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - suggestion_highlight: Option, + hint_highlights: Option, + suggestion_highlights: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -593,7 +595,8 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - suggestion_highlight, + hint_highlights, + suggestion_highlights, ), input_chunk: Default::default(), output_position: output_start, @@ -757,28 +760,18 @@ impl WrapSnapshot { } let text = language::Rope::from(self.text().as_str()); - let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::>(); + let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0); let mut expected_buffer_rows = Vec::new(); - let mut prev_fold_row = 0; + let mut prev_tab_row = 0; for display_row in 0..=self.max_point().row() { let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0)); - let suggestion_point = self - .tab_snapshot - .to_suggestion_point(tab_point, Bias::Left) - .0; - let fold_point = self - .tab_snapshot - .suggestion_snapshot - .to_fold_point(suggestion_point); - if fold_point.row() == prev_fold_row && display_row != 0 { + if tab_point.row() == prev_tab_row && display_row != 0 { expected_buffer_rows.push(None); } else { - let buffer_point = fold_point - .to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot); - expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]); - prev_fold_row = fold_point.row(); + expected_buffer_rows.push(input_buffer_rows.next().unwrap()); } + prev_tab_row = tab_point.row(); assert_eq!(self.line_len(display_row), text.line_len(display_row)); } @@ -1038,7 +1031,7 @@ fn consolidate_wrap_edits(edits: &mut Vec) { mod tests { use super::*; use crate::{ - display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap}, + display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, MultiBuffer, }; use gpui::test::observe; @@ -1089,11 +1082,11 @@ mod tests { }); let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone()); + let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone()); log::info!("FoldMap text: {:?}", fold_snapshot.text()); - let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone()); - log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text()); - let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size); + let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size); let tabs_snapshot = tab_map.set_max_expansion_column(32); log::info!("TabMap text: {:?}", tabs_snapshot.text()); @@ -1122,6 +1115,7 @@ mod tests { ); log::info!("Wrapped text: {:?}", actual_text); + let mut next_inlay_id = 0; let mut edits = Vec::new(); for _i in 0..operations { log::info!("{} ==============================================", _i); @@ -1139,10 +1133,8 @@ mod tests { } 20..=39 => { for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) { - let (suggestion_snapshot, suggestion_edits) = - suggestion_map.sync(fold_snapshot, fold_edits); let (tabs_snapshot, tab_edits) = - tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size); + tab_map.sync(fold_snapshot, fold_edits, tab_size); let (mut snapshot, wrap_edits) = wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); snapshot.check_invariants(); @@ -1151,10 +1143,11 @@ mod tests { } } 40..=59 => { - let (suggestion_snapshot, suggestion_edits) = - suggestion_map.randomly_mutate(&mut rng); + let (inlay_snapshot, inlay_edits) = + inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); let (tabs_snapshot, tab_edits) = - tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size); + tab_map.sync(fold_snapshot, fold_edits, tab_size); let (mut snapshot, wrap_edits) = wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); snapshot.check_invariants(); @@ -1173,13 +1166,12 @@ mod tests { } log::info!("Buffer text: {:?}", buffer_snapshot.text()); - let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits); + let (inlay_snapshot, inlay_edits) = + inlay_map.sync(buffer_snapshot.clone(), buffer_edits); + log::info!("InlayMap text: {:?}", inlay_snapshot.text()); + let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits); log::info!("FoldMap text: {:?}", fold_snapshot.text()); - let (suggestion_snapshot, suggestion_edits) = - suggestion_map.sync(fold_snapshot, fold_edits); - log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text()); - let (tabs_snapshot, tab_edits) = - tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size); + let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size); log::info!("TabMap text: {:?}", tabs_snapshot.text()); let unwrapped_text = tabs_snapshot.text(); @@ -1227,7 +1219,7 @@ mod tests { if tab_size.get() == 1 || !wrapped_snapshot .tab_snapshot - .suggestion_snapshot + .fold_snapshot .text() .contains('\t') { @@ -1328,8 +1320,14 @@ mod tests { } pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1, false, None, None) - .map(|h| h.text) + self.chunks( + wrap_row..self.max_point().row() + 1, + false, + None, + None, + None, + ) + .map(|h| h.text) } fn verify_chunks(&mut self, rng: &mut impl Rng) { @@ -1352,7 +1350,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None) + .chunks(start_row..end_row, true, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2e0d444e04..e979bd9c1e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2,6 +2,7 @@ mod blink_manager; pub mod display_map; mod editor_settings; mod element; +mod inlay_hint_cache; mod git; mod highlight_matching_bracket; @@ -25,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; @@ -52,11 +53,12 @@ use gpui::{ }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; +use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ - language_settings::{self, all_language_settings}, + language_settings::{self, all_language_settings, InlayHintSettings}, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, @@ -64,11 +66,12 @@ use language::{ use link_go_to_definition::{ hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, }; +use log::error; +use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; -use multi_buffer::{MultiBufferChunks, ToOffsetUtf16}; use ordered_float::OrderedFloat; use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; use scroll::{ @@ -85,12 +88,13 @@ use std::{ cmp::{self, Ordering, Reverse}, mem, num::NonZeroU32, - ops::{Deref, DerefMut, Range}, + ops::{ControlFlow, Deref, DerefMut, Range}, path::Path, sync::Arc, time::{Duration, Instant}, }; pub use sum_tree::Bias; +use text::Rope; use theme::{DiagnosticStyle, Theme, ThemeSettings}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ItemNavHistory, ViewId, Workspace}; @@ -180,6 +184,21 @@ pub struct GutterHover { pub hovered: bool, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum InlayId { + Suggestion(usize), + Hint(usize), +} + +impl InlayId { + fn id(&self) -> usize { + match self { + Self::Suggestion(id) => *id, + Self::Hint(id) => *id, + } + } +} + actions!( editor, [ @@ -206,6 +225,7 @@ actions!( DuplicateLine, MoveLineUp, MoveLineDown, + JoinLines, Transpose, Cut, Copy, @@ -321,6 +341,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::indent); cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); + cx.add_action(Editor::join_lines); cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_next_word_end); @@ -533,6 +554,8 @@ pub struct Editor { gutter_hovered: bool, link_go_to_definition_state: LinkGoToDefinitionState, copilot_state: CopilotState, + inlay_hint_cache: InlayHintCache, + next_inlay_id: usize, _subscriptions: Vec, } @@ -1054,6 +1077,7 @@ pub struct CopilotState { cycled: bool, completions: Vec, active_completion_index: usize, + suggestion: Option, } impl Default for CopilotState { @@ -1065,6 +1089,7 @@ impl Default for CopilotState { completions: Default::default(), active_completion_index: 0, cycled: false, + suggestion: None, } } } @@ -1179,6 +1204,14 @@ enum GotoDefinitionKind { Type, } +#[derive(Debug, Clone)] +enum InlayRefreshReason { + SettingsChange(InlayHintSettings), + NewLinesShown, + BufferEdited(HashSet>), + RefreshRequested, +} + impl Editor { pub fn single_line( field_editor_style: Option>, @@ -1280,15 +1313,28 @@ impl Editor { let soft_wrap_mode_override = (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None); - let mut project_subscription = None; - if mode == EditorMode::Full && buffer.read(cx).is_singleton() { + let mut project_subscriptions = Vec::new(); + if mode == EditorMode::Full { if let Some(project) = project.as_ref() { - project_subscription = Some(cx.observe(project, |_, _, cx| { - cx.emit(Event::TitleChanged); - })) + if buffer.read(cx).is_singleton() { + project_subscriptions.push(cx.observe(project, |_, _, cx| { + cx.emit(Event::TitleChanged); + })); + } + project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { + if let project::Event::RefreshInlays = event { + editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx); + }; + })); } } + let inlay_hint_settings = inlay_hint_settings( + selections.newest_anchor().head(), + &buffer.read(cx).snapshot(cx), + cx, + ); + let mut this = Self { handle: cx.weak_handle(), buffer: buffer.clone(), @@ -1322,6 +1368,7 @@ impl Editor { .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)), completion_tasks: Default::default(), next_completion_id: 0, + next_inlay_id: 0, available_code_actions: Default::default(), code_actions_task: Default::default(), document_highlights_task: Default::default(), @@ -1338,6 +1385,7 @@ impl Editor { hover_state: Default::default(), link_go_to_definition_state: Default::default(), copilot_state: Default::default(), + inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), @@ -1348,9 +1396,7 @@ impl Editor { ], }; - if let Some(project_subscription) = project_subscription { - this._subscriptions.push(project_subscription); - } + this._subscriptions.extend(project_subscriptions); this.end_selection(cx); this.scroll_manager.show_scrollbar(cx); @@ -1871,7 +1917,7 @@ impl Editor { s.set_pending(pending, mode); }); } else { - log::error!("update_selection dispatched with no pending selection"); + error!("update_selection dispatched with no pending selection"); return; } @@ -1989,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(); @@ -2047,6 +2094,7 @@ impl Editor { selection.range(), format!("{}{}", text, bracket_pair.end).into(), )); + brace_inserted = true; continue; } } @@ -2073,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, @@ -2140,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::(cx).use_on_type_format { + if !brace_inserted && settings::get::(cx).use_on_type_format { if let Some(on_type_format_task) = this.trigger_on_type_formatting(text.to_string(), cx) { @@ -2575,6 +2623,108 @@ impl Editor { } } + fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext) { + if self.project.is_none() || self.mode != EditorMode::Full { + return; + } + + let (invalidate_cache, required_languages) = match reason { + InlayRefreshReason::SettingsChange(new_settings) => { + match self.inlay_hint_cache.update_settings( + &self.buffer, + new_settings, + self.visible_inlay_hints(cx), + cx, + ) { + ControlFlow::Break(Some(InlaySplice { + to_remove, + to_insert, + })) => { + self.splice_inlay_hints(to_remove, to_insert, cx); + return; + } + ControlFlow::Break(None) => return, + ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), + } + } + 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(required_languages.as_ref(), cx), + invalidate_cache, + cx, + ) + } + + fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec { + self.display_map + .read(cx) + .current_inlays() + .filter(move |inlay| { + Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id) + }) + .cloned() + .collect() + } + + fn excerpt_visible_offsets( + &self, + restrict_to_languages: Option<&HashSet>>, + cx: &mut ViewContext<'_, '_, Editor>, + ) -> HashMap, Global, Range)> { + let multi_buffer = self.buffer().read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + let multi_buffer_visible_start = self + .scroll_manager + .anchor() + .anchor + .to_point(&multi_buffer_snapshot); + let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( + multi_buffer_visible_start + + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), + Bias::Left, + ); + let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; + multi_buffer + .range_to_buffer_ranges(multi_buffer_visible_range, cx) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .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() + } + + fn splice_inlay_hints( + &self, + to_remove: Vec, + to_insert: Vec, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |display_map, cx| { + display_map.splice_inlays(to_remove, to_insert, cx); + }); + } + fn trigger_on_type_formatting( &self, input: String, @@ -3225,10 +3375,7 @@ impl Editor { } fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { - if let Some(suggestion) = self - .display_map - .update(cx, |map, cx| map.replace_suggestion::(None, cx)) - { + if let Some(suggestion) = self.take_active_copilot_suggestion(cx) { if let Some((copilot, completion)) = Copilot::global(cx).zip(self.copilot_state.active_completion()) { @@ -3247,7 +3394,7 @@ impl Editor { } fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext) -> bool { - if self.has_active_copilot_suggestion(cx) { + if let Some(suggestion) = self.take_active_copilot_suggestion(cx) { if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| { @@ -3258,8 +3405,9 @@ impl Editor { self.report_copilot_event(None, false, cx) } - self.display_map - .update(cx, |map, cx| map.replace_suggestion::(None, cx)); + self.display_map.update(cx, |map, cx| { + map.splice_inlays(vec![suggestion.id], Vec::new(), cx) + }); cx.notify(); true } else { @@ -3280,7 +3428,26 @@ impl Editor { } fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool { - self.display_map.read(cx).has_suggestion() + if let Some(suggestion) = self.copilot_state.suggestion.as_ref() { + let buffer = self.buffer.read(cx).read(cx); + suggestion.position.is_valid(&buffer) + } else { + false + } + } + + fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext) -> Option { + let suggestion = self.copilot_state.suggestion.take()?; + self.display_map.update(cx, |map, cx| { + map.splice_inlays(vec![suggestion.id], Default::default(), cx); + }); + let buffer = self.buffer.read(cx).read(cx); + + if suggestion.position.is_valid(&buffer) { + Some(suggestion) + } else { + None + } } fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext) { @@ -3297,14 +3464,17 @@ impl Editor { .copilot_state .text_for_active_completion(cursor, &snapshot) { + let text = Rope::from(text); + let mut to_remove = Vec::new(); + if let Some(suggestion) = self.copilot_state.suggestion.take() { + to_remove.push(suggestion.id); + } + + 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.replace_suggestion( - Some(Suggestion { - position: cursor, - text: text.trim_end().into(), - }), - cx, - ) + map.splice_inlays(to_remove, vec![suggestion_inlay], cx) }); cx.notify(); } else { @@ -3320,15 +3490,21 @@ impl Editor { pub fn render_code_actions_indicator( &self, style: &EditorStyle, - active: bool, + is_active: bool, cx: &mut ViewContext, ) -> Option> { if self.available_code_actions.is_some() { enum CodeActions {} Some( MouseEventHandler::::new(0, cx, |state, _| { - Svg::new("icons/bolt_8.svg") - .with_color(style.code_actions.indicator.style_for(state, active).color) + Svg::new("icons/bolt_8.svg").with_color( + style + .code_actions + .indicator + .in_state(is_active) + .style_for(state) + .color, + ) }) .with_cursor_style(CursorStyle::PointingHand) .with_padding(Padding::uniform(3.)) @@ -3378,10 +3554,8 @@ impl Editor { .with_color( style .indicator - .style_for( - mouse_state, - fold_status == FoldStatus::Folded, - ) + .in_state(fold_status == FoldStatus::Folded) + .style_for(mouse_state) .color, ) .constrained() @@ -3952,6 +4126,60 @@ impl Editor { }); } + pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext) { + let mut row_ranges = Vec::>::new(); + for selection in self.selections.all::(cx) { + let start = selection.start.row; + let end = if selection.start.row == selection.end.row { + selection.start.row + 1 + } else { + selection.end.row + }; + + if let Some(last_row_range) = row_ranges.last_mut() { + if start <= last_row_range.end { + last_row_range.end = end; + continue; + } + } + row_ranges.push(start..end); + } + + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut cursor_positions = Vec::new(); + for row_range in &row_ranges { + let anchor = snapshot.anchor_before(Point::new( + row_range.end - 1, + snapshot.line_len(row_range.end - 1), + )); + cursor_positions.push(anchor.clone()..anchor); + } + + self.transact(cx, |this, cx| { + for row_range in row_ranges.into_iter().rev() { + for row in row_range.rev() { + let end_of_line = Point::new(row, snapshot.line_len(row)); + let indent = snapshot.indent_size_for_line(row + 1); + let start_of_next_line = Point::new(row + 1, indent.len); + + let replace = if snapshot.line_len(row + 1) > indent.len { + " " + } else { + "" + }; + + this.buffer.update(cx, |buffer, cx| { + buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) + }); + } + } + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_anchor_ranges(cursor_positions) + }); + }); + } + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -6581,7 +6809,7 @@ impl Editor { if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) { *end_selections = Some(self.selections.disjoint_anchors()); } else { - log::error!("unexpectedly ended a transaction that wasn't started by this editor"); + error!("unexpectedly ended a transaction that wasn't started by this editor"); } cx.emit(Event::Edited); @@ -7031,7 +7259,7 @@ impl Editor { fn on_buffer_event( &mut self, - _: ModelHandle, + multibuffer: ModelHandle, event: &multi_buffer::Event, cx: &mut ViewContext, ) { @@ -7043,6 +7271,33 @@ impl Editor { self.update_visible_copilot_suggestion(cx); } cx.emit(Event::BufferEdited); + + 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::>(); + if !languages_affected.is_empty() { + self.refresh_inlays( + InlayRefreshReason::BufferEdited(languages_affected), + cx, + ); + } + } } multi_buffer::Event::ExcerptsAdded { buffer, @@ -7067,7 +7322,7 @@ impl Editor { self.refresh_active_diagnostics(cx); } _ => {} - } + }; } fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { @@ -7076,6 +7331,14 @@ impl Editor { fn settings_changed(&mut self, cx: &mut ViewContext) { self.refresh_copilot_suggestions(true, cx); + self.refresh_inlays( + InlayRefreshReason::SettingsChange(inlay_hint_settings( + self.selections.newest_anchor().head(), + &self.buffer.read(cx).snapshot(cx), + cx, + )), + cx, + ); } pub fn set_searchable(&mut self, searchable: bool) { @@ -7365,6 +7628,23 @@ impl Editor { let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; }; cx.write_to_clipboard(ClipboardItem::new(lines)); } + + pub fn inlay_hint_cache(&self) -> &InlayHintCache { + &self.inlay_hint_cache + } +} + +fn inlay_hint_settings( + location: Anchor, + snapshot: &MultiBufferSnapshot, + cx: &mut ViewContext<'_, '_, Editor>, +) -> InlayHintSettings { + let file = snapshot.file_at(location); + let language = snapshot.language_at(location); + let settings = all_language_settings(file, cx); + settings + .language(language.map(|l| l.name()).as_deref()) + .inlay_hints } fn consume_contiguous_rows( @@ -7581,8 +7861,14 @@ impl View for Editor { keymap.add_identifier("renaming"); } match self.context_menu.as_ref() { - Some(ContextMenu::Completions(_)) => keymap.add_identifier("showing_completions"), - Some(ContextMenu::CodeActions(_)) => keymap.add_identifier("showing_code_actions"), + Some(ContextMenu::Completions(_)) => { + keymap.add_identifier("menu"); + keymap.add_identifier("showing_completions") + } + Some(ContextMenu::CodeActions(_)) => { + keymap.add_identifier("menu"); + keymap.add_identifier("showing_code_actions") + } None => {} } for layer in self.keymap_context_layers.values() { @@ -7949,6 +8235,7 @@ impl Deref for EditorStyle { pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock { let mut highlighted_lines = Vec::new(); + for (index, line) in diagnostic.message.lines().enumerate() { let line = match &diagnostic.source { Some(source) if index == 0 => { @@ -7960,25 +8247,44 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend }; highlighted_lines.push(line); } - + let message = diagnostic.message; Arc::new(move |cx: &mut BlockContext| { + let message = message.clone(); let settings = settings::get::(cx); + let tooltip_style = settings.theme.tooltip.clone(); let theme = &settings.theme.editor; let style = diagnostic_style(diagnostic.severity, is_valid, theme); let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); - Flex::column() - .with_children(highlighted_lines.iter().map(|(line, highlights)| { - Label::new( - line.clone(), - style.message.clone().with_font_size(font_size), - ) - .with_highlights(highlights.clone()) - .contained() - .with_margin_left(cx.anchor_x) - })) - .aligned() - .left() - .into_any() + let anchor_x = cx.anchor_x; + enum BlockContextToolip {} + MouseEventHandler::::new(cx.block_id, cx, |_, _| { + Flex::column() + .with_children(highlighted_lines.iter().map(|(line, highlights)| { + Label::new( + line.clone(), + style.message.clone().with_font_size(font_size), + ) + .with_highlights(highlights.clone()) + .contained() + .with_margin_left(anchor_x) + })) + .aligned() + .left() + .into_any() + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new(message.clone())); + }) + // We really need to rethink this ID system... + .with_tooltip::( + cx.block_id, + "Copy diagnostic message".to_string(), + None, + tooltip_style, + cx, + ) + .into_any() }) } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e2b876f4b7..9e726d6cc4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1,7 +1,11 @@ use super::*; -use crate::test::{ - assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, - editor_test_context::EditorTestContext, select_ranges, +use crate::{ + scroll::scroll_amount::ScrollAmount, + test::{ + assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext, + editor_test_context::EditorTestContext, select_ranges, + }, + JoinLines, }; use drag_and_drop::DragAndDrop; use futures::StreamExt; @@ -1356,6 +1360,43 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon ); } +#[gpui::test] +async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); + cx.simulate_window_resize(cx.window_id, vec2f(1000., 4. * line_height + 0.5)); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + editor.scroll_screen(&ScrollAmount::Page(1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.)); + editor.scroll_screen(&ScrollAmount::Page(-1.), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.)); + editor.scroll_screen(&ScrollAmount::Page(0.5), cx); + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)); + }); +} + #[gpui::test] async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -2325,6 +2366,137 @@ fn test_delete_line(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 0)..Point::new(0, 0)] + ); + + // When on single line, replace newline at end by space + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 3)..Point::new(0, 3)] + ); + + // When multiple lines are selected, remove newlines that are spanned by the selection + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 5)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 11)..Point::new(0, 11)] + ); + + // Undo should be transactional + editor.undo(&Undo, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); + + // When joining an empty line don't insert a space + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(2, 1)..Point::new(2, 2)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + + // We can remove trailing newlines + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + + // We don't blow up on the last line + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd"); + assert_eq!( + editor.selections.ranges::(cx), + [Point::new(2, 3)..Point::new(2, 3)] + ); + + // reset to test indentation + editor.buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(1, 0)..Point::new(1, 2), " "), + (Point::new(2, 0)..Point::new(2, 3), " \n\td"), + ], + None, + cx, + ) + }); + + // We remove any leading spaces + assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td"); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) + }); + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td"); + + // We don't insert a space for a line containing only spaces + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td"); + + // We ignore any leading tabs + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb c d"); + + editor + }); +} + +#[gpui::test] +fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 2)..Point::new(1, 1), + Point::new(1, 2)..Point::new(1, 2), + Point::new(3, 1)..Point::new(3, 2), + ]) + }); + + editor.join_lines(&JoinLines, cx); + assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n"); + + assert_eq!( + editor.selections.ranges::(cx), + [ + Point::new(0, 7)..Point::new(0, 7), + Point::new(1, 3)..Point::new(1, 3) + ] + ); + editor + }); +} + #[gpui::test] fn test_duplicate_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -6807,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::() + .unwrap(); + + fake_server.handle_request::(|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 { let point = DisplayPoint::new(row as u32, column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5f7843f721..0a462b1e5d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1433,7 +1433,12 @@ impl EditorElement { } else { let style = &self.style; let chunks = snapshot - .chunks(rows.clone(), true, Some(style.theme.suggestion)) + .chunks( + rows.clone(), + true, + Some(style.theme.hint), + Some(style.theme.suggestion), + ) .map(|chunk| { let mut highlight_style = chunk .syntax_highlight_id @@ -1508,6 +1513,7 @@ impl EditorElement { editor: &mut Editor, cx: &mut LayoutContext, ) -> (f32, Vec) { + let mut block_id = 0; let scroll_x = snapshot.scroll_anchor.offset.x(); let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) @@ -1515,7 +1521,7 @@ impl EditorElement { TransformBlock::ExcerptHeader { .. } => false, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, }); - let mut render_block = |block: &TransformBlock, width: f32| { + let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| { let mut element = match block { TransformBlock::Custom(block) => { let align_to = block @@ -1540,6 +1546,7 @@ impl EditorElement { scroll_x, gutter_width, em_width, + block_id, }) } TransformBlock::ExcerptHeader { @@ -1568,7 +1575,7 @@ impl EditorElement { enum JumpIcon {} MouseEventHandler::::new((*id).into(), cx, |state, _| { - let style = style.jump_icon.style_for(state, false); + let style = style.jump_icon.style_for(state); Svg::new("icons/arrow_up_right_8.svg") .with_color(style.color) .constrained() @@ -1675,7 +1682,8 @@ impl EditorElement { let mut fixed_block_max_width = 0f32; let mut blocks = Vec::new(); for (row, block) in fixed_blocks { - let element = render_block(block, f32::INFINITY); + let element = render_block(block, f32::INFINITY, block_id); + block_id += 1; fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width); blocks.push(BlockLayout { row, @@ -1695,7 +1703,8 @@ impl EditorElement { .max(gutter_width + scroll_width), BlockStyle::Fixed => unreachable!(), }; - let element = render_block(block, width); + let element = render_block(block, width, block_id); + block_id += 1; blocks.push(BlockLayout { row, element, @@ -1958,7 +1967,7 @@ impl Element for EditorElement { let em_advance = style.text.em_advance(cx.font_cache()); let overscroll = vec2f(em_width, 0.); let snapshot = { - editor.set_visible_line_count(size.y() / line_height); + editor.set_visible_line_count(size.y() / line_height, cx); let editor_width = text_width - gutter_margin - overscroll.x() - em_width; let wrap_width = match editor.soft_wrap_mode(cx) { @@ -2131,7 +2140,7 @@ impl Element for EditorElement { .folds .ellipses .background - .style_for(&mut cx.mouse_state::(id as usize), false) + .style_for(&mut cx.mouse_state::(id as usize)) .color; (id, fold, color) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs new file mode 100644 index 0000000000..a6ea3962d2 --- /dev/null +++ b/crates/editor/src/inlay_hint_cache.rs @@ -0,0 +1,2275 @@ +use std::{ + cmp, + ops::{ControlFlow, Range}, + sync::Arc, +}; + +use crate::{ + display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot, +}; +use anyhow::Context; +use clock::Global; +use gpui::{ModelHandle, Task, ViewContext}; +use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; +use log::error; +use parking_lot::RwLock; +use project::InlayHint; + +use collections::{hash_map, HashMap, HashSet}; +use language::language_settings::InlayHintSettings; +use util::post_inc; + +pub struct InlayHintCache { + pub hints: HashMap>>, + pub allowed_hint_kinds: HashSet>, + pub version: usize, + pub enabled: bool, + update_tasks: HashMap, +} + +#[derive(Debug)] +pub struct CachedExcerptHints { + version: usize, + buffer_version: Global, + buffer_id: u64, + pub hints: Vec<(InlayId, InlayHint)>, +} + +#[derive(Debug, Clone, Copy)] +pub enum InvalidationStrategy { + RefreshRequested, + BufferEdited, + None, +} + +#[derive(Debug, Default)] +pub struct InlaySplice { + pub to_remove: Vec, + pub to_insert: Vec, +} + +struct UpdateTask { + invalidate: InvalidationStrategy, + cache_version: usize, + task: RunningTask, + pending_refresh: Option, +} + +struct RunningTask { + _task: Task<()>, + is_running_rx: smol::channel::Receiver<()>, +} + +#[derive(Debug)] +struct ExcerptHintsUpdate { + excerpt_id: ExcerptId, + remove_from_visible: Vec, + remove_from_cache: HashSet, + add_to_cache: HashSet, +} + +#[derive(Debug, Clone, Copy)] +struct ExcerptQuery { + buffer_id: u64, + excerpt_id: ExcerptId, + dimensions: ExcerptDimensions, + cache_version: usize, + invalidate: InvalidationStrategy, +} + +#[derive(Debug, Clone, Copy)] +struct ExcerptDimensions { + excerpt_range_start: language::Anchor, + excerpt_range_end: language::Anchor, + excerpt_visible_range_start: language::Anchor, + excerpt_visible_range_end: language::Anchor, +} + +struct HintFetchRanges { + visible_range: Range, + other_ranges: Vec>, +} + +impl InvalidationStrategy { + fn should_invalidate(&self) -> bool { + matches!( + self, + InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited + ) + } +} + +impl ExcerptQuery { + fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { + let visible_range = + self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; + let mut other_ranges = Vec::new(); + if self + .dimensions + .excerpt_range_start + .cmp(&visible_range.start, buffer) + .is_lt() + { + let mut end = visible_range.start; + end.offset -= 1; + other_ranges.push(self.dimensions.excerpt_range_start..end); + } + if self + .dimensions + .excerpt_range_end + .cmp(&visible_range.end, buffer) + .is_gt() + { + let mut start = visible_range.end; + start.offset += 1; + other_ranges.push(start..self.dimensions.excerpt_range_end); + } + + HintFetchRanges { + visible_range, + other_ranges: other_ranges.into_iter().map(|range| range).collect(), + } + } +} + +impl InlayHintCache { + pub fn new(inlay_hint_settings: InlayHintSettings) -> Self { + Self { + allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(), + enabled: inlay_hint_settings.enabled, + hints: HashMap::default(), + update_tasks: HashMap::default(), + version: 0, + } + } + + pub fn update_settings( + &mut self, + multi_buffer: &ModelHandle, + new_hint_settings: InlayHintSettings, + visible_hints: Vec, + cx: &mut ViewContext, + ) -> ControlFlow> { + let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds(); + match (self.enabled, new_hint_settings.enabled) { + (false, false) => { + self.allowed_hint_kinds = new_allowed_hint_kinds; + ControlFlow::Break(None) + } + (true, true) => { + if new_allowed_hint_kinds == self.allowed_hint_kinds { + ControlFlow::Break(None) + } else { + let new_splice = self.new_allowed_hint_kinds_splice( + multi_buffer, + &visible_hints, + &new_allowed_hint_kinds, + cx, + ); + if new_splice.is_some() { + self.version += 1; + self.update_tasks.clear(); + self.allowed_hint_kinds = new_allowed_hint_kinds; + } + ControlFlow::Break(new_splice) + } + } + (true, false) => { + self.enabled = new_hint_settings.enabled; + self.allowed_hint_kinds = new_allowed_hint_kinds; + if self.hints.is_empty() { + ControlFlow::Break(None) + } else { + self.clear(); + ControlFlow::Break(Some(InlaySplice { + to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(), + to_insert: Vec::new(), + })) + } + } + (false, true) => { + self.enabled = new_hint_settings.enabled; + self.allowed_hint_kinds = new_allowed_hint_kinds; + ControlFlow::Continue(()) + } + } + } + + pub fn refresh_inlay_hints( + &mut self, + mut excerpts_to_query: HashMap, Global, Range)>, + invalidate: InvalidationStrategy, + cx: &mut ViewContext, + ) { + if !self.enabled || excerpts_to_query.is_empty() { + return; + } + let update_tasks = &mut self.update_tasks; + if invalidate.should_invalidate() { + update_tasks + .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); + } + let cache_version = self.version; + excerpts_to_query.retain(|visible_excerpt_id, _| { + match update_tasks.entry(*visible_excerpt_id) { + hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) { + cmp::Ordering::Less => true, + cmp::Ordering::Equal => invalidate.should_invalidate(), + cmp::Ordering::Greater => false, + }, + hash_map::Entry::Vacant(_) => true, + } + }); + + cx.spawn(|editor, mut cx| async move { + editor + .update(&mut cx, |editor, cx| { + spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx) + }) + .ok(); + }) + .detach(); + } + + fn new_allowed_hint_kinds_splice( + &self, + multi_buffer: &ModelHandle, + visible_hints: &[Inlay], + new_kinds: &HashSet>, + cx: &mut ViewContext, + ) -> Option { + let old_kinds = &self.allowed_hint_kinds; + if new_kinds == old_kinds { + return None; + } + + let mut to_remove = Vec::new(); + let mut to_insert = Vec::new(); + let mut shown_hints_to_remove = visible_hints.iter().fold( + HashMap::>::default(), + |mut current_hints, inlay| { + current_hints + .entry(inlay.position.excerpt_id) + .or_default() + .push((inlay.position, inlay.id)); + current_hints + }, + ); + + let multi_buffer = multi_buffer.read(cx); + let multi_buffer_snapshot = multi_buffer.snapshot(cx); + + for (excerpt_id, excerpt_cached_hints) in &self.hints { + let shown_excerpt_hints_to_remove = + shown_hints_to_remove.entry(*excerpt_id).or_default(); + let excerpt_cached_hints = excerpt_cached_hints.read(); + let mut excerpt_cache = excerpt_cached_hints.hints.iter().fuse().peekable(); + shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { + let Some(buffer) = shown_anchor + .buffer_id + .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false }; + let buffer_snapshot = buffer.read(cx).snapshot(); + loop { + match excerpt_cache.peek() { + Some((cached_hint_id, cached_hint)) => { + if cached_hint_id == shown_hint_id { + excerpt_cache.next(); + return !new_kinds.contains(&cached_hint.kind); + } + + match cached_hint + .position + .cmp(&shown_anchor.text_anchor, &buffer_snapshot) + { + cmp::Ordering::Less | cmp::Ordering::Equal => { + if !old_kinds.contains(&cached_hint.kind) + && new_kinds.contains(&cached_hint.kind) + { + to_insert.push(Inlay::hint( + cached_hint_id.id(), + multi_buffer_snapshot.anchor_in_excerpt( + *excerpt_id, + cached_hint.position, + ), + &cached_hint, + )); + } + excerpt_cache.next(); + } + cmp::Ordering::Greater => return true, + } + } + None => return true, + } + } + }); + + 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(Inlay::hint( + cached_hint_id.id(), + multi_buffer_snapshot + .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position), + &maybe_missed_cached_hint, + )); + } + } + } + + to_remove.extend( + shown_hints_to_remove + .into_values() + .flatten() + .map(|(_, hint_id)| hint_id), + ); + if to_remove.is_empty() && to_insert.is_empty() { + None + } else { + Some(InlaySplice { + to_remove, + to_insert, + }) + } + } + + fn clear(&mut self) { + self.version += 1; + self.update_tasks.clear(); + self.hints.clear(); + } +} + +fn spawn_new_update_tasks( + editor: &mut Editor, + excerpts_to_query: HashMap, Global, Range)>, + 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, 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 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; + } + }; + + 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), + ) + }); + + 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, + }); + } + } + } + } +} + +fn new_update_task( + query: ExcerptQuery, + multi_buffer_snapshot: MultiBufferSnapshot, + buffer_snapshot: BufferSnapshot, + visible_hints: Arc>, + cached_excerpt_hints: Option>>, + is_refresh_after_regular_task: bool, + cx: &mut ViewContext<'_, '_, Editor>, +) -> RunningTask { + let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot); + let (is_running_tx, is_running_rx) = smol::channel::bounded(1); + let _task = cx.spawn(|editor, mut cx| async move { + let _is_running_tx = is_running_tx; + let create_update_task = |range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + range, + cx.clone(), + ) + }; + + if is_refresh_after_regular_task { + let visible_range_has_updates = + match create_update_task(hints_fetch_ranges.visible_range).await { + Ok(updated) => updated, + Err(e) => { + error!("inlay hint visible range update task failed: {e:#}"); + return; + } + }; + + if visible_range_has_updates { + let other_update_results = futures::future::join_all( + hints_fetch_ranges + .other_ranges + .into_iter() + .map(create_update_task), + ) + .await; + + for result in other_update_results { + if let Err(e) = result { + error!("inlay hint update task failed: {e:#}"); + } + } + } + } else { + let task_update_results = futures::future::join_all( + std::iter::once(hints_fetch_ranges.visible_range) + .chain(hints_fetch_ranges.other_ranges.into_iter()) + .map(create_update_task), + ) + .await; + + for result in task_update_results { + if let Err(e) = result { + error!("inlay hint update task failed: {e:#}"); + } + } + } + + editor + .update(&mut cx, |editor, cx| { + let pending_refresh_query = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + .and_then(|task| task.pending_refresh.take()); + + if let Some(pending_refresh_query) = pending_refresh_query { + let refresh_multi_buffer = editor.buffer().read(cx); + let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx); + let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx)); + let refresh_cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .get(&pending_refresh_query.excerpt_id) + .map(Arc::clone); + if let Some(buffer) = + refresh_multi_buffer.buffer(pending_refresh_query.buffer_id) + { + drop(refresh_multi_buffer); + editor.inlay_hint_cache.update_tasks.insert( + pending_refresh_query.excerpt_id, + UpdateTask { + invalidate: InvalidationStrategy::RefreshRequested, + cache_version: editor.inlay_hint_cache.version, + task: new_update_task( + pending_refresh_query, + refresh_multi_buffer_snapshot, + buffer.read(cx).snapshot(), + refresh_visible_hints, + refresh_cached_excerpt_hints, + true, + cx, + ), + pending_refresh: None, + }, + ); + } + } + }) + .ok(); + }); + + RunningTask { + _task, + is_running_rx, + } +} + +async fn fetch_and_update_hints( + editor: gpui::WeakViewHandle, + multi_buffer_snapshot: MultiBufferSnapshot, + buffer_snapshot: BufferSnapshot, + visible_hints: Arc>, + cached_excerpt_hints: Option>>, + query: ExcerptQuery, + fetch_range: Range, + mut cx: gpui::AsyncAppContext, +) -> anyhow::Result { + let inlay_hints_fetch_task = editor + .update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .buffer(query.buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.inlay_hints(buffer, fetch_range.clone(), cx) + })) + }) + }) + .ok() + .flatten(); + let mut update_happened = false; + let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) }; + let new_hints = inlay_hints_fetch_task + .await + .context("inlay hint fetch task")?; + let background_task_buffer_snapshot = buffer_snapshot.clone(); + let backround_fetch_range = fetch_range.clone(); + let new_update = cx + .background() + .spawn(async move { + calculate_hint_updates( + query, + backround_fetch_range, + new_hints, + &background_task_buffer_snapshot, + cached_excerpt_hints, + &visible_hints, + ) + }) + .await; + + editor + .update(&mut cx, |editor, cx| { + if let Some(new_update) = new_update { + update_happened = !new_update.add_to_cache.is_empty() + || !new_update.remove_from_cache.is_empty() + || !new_update.remove_from_visible.is_empty(); + + let cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .entry(new_update.excerpt_id) + .or_insert_with(|| { + Arc::new(RwLock::new(CachedExcerptHints { + version: query.cache_version, + buffer_version: buffer_snapshot.version().clone(), + buffer_id: query.buffer_id, + hints: Vec::new(), + })) + }); + let mut cached_excerpt_hints = cached_excerpt_hints.write(); + match query.cache_version.cmp(&cached_excerpt_hints.version) { + cmp::Ordering::Less => return, + cmp::Ordering::Greater | cmp::Ordering::Equal => { + cached_excerpt_hints.version = query.cache_version; + } + } + cached_excerpt_hints + .hints + .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + editor.inlay_hint_cache.version += 1; + + let mut splice = InlaySplice { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + + 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 = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + splice.to_insert.push(Inlay::hint( + new_inlay_id, + new_hint_position, + &new_hint, + )); + } + + cached_excerpt_hints + .hints + .push((InlayId::Hint(new_inlay_id), new_hint)); + } + + cached_excerpt_hints + .hints + .sort_by(|(_, hint_a), (_, hint_b)| { + hint_a.position.cmp(&hint_b.position, &buffer_snapshot) + }); + drop(cached_excerpt_hints); + + if query.invalidate.should_invalidate() { + let mut outdated_excerpt_caches = HashSet::default(); + for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() { + let excerpt_hints = excerpt_hints.read(); + if excerpt_hints.buffer_id == query.buffer_id + && excerpt_id != &query.excerpt_id + && buffer_snapshot + .version() + .changed_since(&excerpt_hints.buffer_version) + { + outdated_excerpt_caches.insert(*excerpt_id); + splice + .to_remove + .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); + } + } + editor + .inlay_hint_cache + .hints + .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); + } + + let InlaySplice { + to_remove, + to_insert, + } = splice; + if !to_remove.is_empty() || !to_insert.is_empty() { + editor.splice_inlay_hints(to_remove, to_insert, cx) + } + } + }) + .ok(); + + Ok(update_happened) +} + +fn calculate_hint_updates( + query: ExcerptQuery, + fetch_range: Range, + new_excerpt_hints: Vec, + buffer_snapshot: &BufferSnapshot, + cached_excerpt_hints: Option>>, + visible_hints: &[Inlay], +) -> Option { + let mut add_to_cache: HashSet = HashSet::default(); + let mut excerpt_hints_to_persist = HashMap::default(); + for new_hint in new_excerpt_hints { + if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { + continue; + } + let missing_from_cache = match &cached_excerpt_hints { + Some(cached_excerpt_hints) => { + let cached_excerpt_hints = cached_excerpt_hints.read(); + match cached_excerpt_hints.hints.binary_search_by(|probe| { + probe.1.position.cmp(&new_hint.position, buffer_snapshot) + }) { + Ok(ix) => { + let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix]; + if cached_hint == &new_hint { + excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); + false + } else { + true + } + } + Err(_) => true, + } + } + None => true, + }; + if missing_from_cache { + add_to_cache.insert(new_hint); + } + } + + let mut remove_from_visible = Vec::new(); + let mut remove_from_cache = HashSet::default(); + if query.invalidate.should_invalidate() { + remove_from_visible.extend( + visible_hints + .iter() + .filter(|hint| hint.position.excerpt_id == query.excerpt_id) + .filter(|hint| { + contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot) + }) + .filter(|hint| { + fetch_range + .start + .cmp(&hint.position.text_anchor, buffer_snapshot) + .is_le() + && fetch_range + .end + .cmp(&hint.position.text_anchor, buffer_snapshot) + .is_ge() + }) + .map(|inlay_hint| inlay_hint.id) + .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), + ); + + if let Some(cached_excerpt_hints) = &cached_excerpt_hints { + let cached_excerpt_hints = cached_excerpt_hints.read(); + remove_from_cache.extend( + cached_excerpt_hints + .hints + .iter() + .filter(|(cached_inlay_id, _)| { + !excerpt_hints_to_persist.contains_key(cached_inlay_id) + }) + .filter(|(_, cached_hint)| { + fetch_range + .start + .cmp(&cached_hint.position, buffer_snapshot) + .is_le() + && fetch_range + .end + .cmp(&cached_hint.position, buffer_snapshot) + .is_ge() + }) + .map(|(cached_inlay_id, _)| *cached_inlay_id), + ); + } + } + + if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() { + None + } else { + Some(ExcerptHintsUpdate { + excerpt_id: query.excerpt_id, + remove_from_visible, + remove_from_cache, + add_to_cache, + }) + } +} + +fn contains_position( + range: &Range, + position: language::Anchor, + buffer_snapshot: &BufferSnapshot, +) -> bool { + range.start.cmp(&position, buffer_snapshot).is_le() + && range.end.cmp(&position, buffer_snapshot).is_ge() +} + +#[cfg(test)] +mod tests { + use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + + use crate::{ + scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, + serde_json::json, + ExcerptRange, InlayHintSettings, + }; + use futures::StreamExt; + use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; + use language::{ + language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, + }; + use lsp::FakeLanguageServer; + use parking_lot::Mutex; + use project::{FakeFs, Project}; + use settings::SettingsStore; + use text::Point; + use workspace::Workspace; + + use crate::editor_tests::update_test_settings; + + use super::*; + + #[gpui::test] + async fn test_basic_cache_update_with_duplicate_hints(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), + }) + }); + + 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::(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); + let mut new_hints = Vec::with_capacity(2 * current_call_id as usize); + for _ in 0..2 { + let mut i = current_call_id; + loop { + new_hints.push(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, + }); + if i == 0 { + break; + } + i -= 1; + } + } + + Ok(Some(new_hints)) + } + }) + .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.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input("some change", cx); + edits_made += 1; + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec!["0".to_string(), "1".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should get new hints after an 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, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + edits_made += 1; + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should get new hints after hint refresh/ request" + ); + 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, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + } + + #[gpui::test] + async fn test_no_hint_updates_for_unrelated_language_files(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), + }) + }); + + 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::() + .unwrap(); + let rs_lsp_request_count = Arc::new(AtomicU32::new(0)); + rs_fake_server + .handle_request::(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.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + 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::() + .unwrap(); + let md_lsp_request_count = Arc::new(AtomicU32::new(0)); + md_fake_server + .handle_request::(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.allowed_hint_kinds, allowed_hint_kinds); + 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.allowed_hint_kinds, allowed_hint_kinds); + 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.allowed_hint_kinds, allowed_hint_kinds); + 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.allowed_hint_kinds, allowed_hint_kinds); + 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.allowed_hint_kinds, allowed_hint_kinds); + 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)]); + 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), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst); + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + Ok(Some(vec![ + lsp::InlayHint { + position: lsp::Position::new(0, 1), + label: lsp::InlayHintLabel::String("type hint".to_string()), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 2), + label: lsp::InlayHintLabel::String("parameter hint".to_string()), + kind: Some(lsp::InlayHintKind::PARAMETER), + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }, + lsp::InlayHint { + position: lsp::Position::new(0, 3), + label: lsp::InlayHintLabel::String("other hint".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| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 1, + "Should query new hints once" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its first hints when opening the editor" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor update the cache version after every cache/view change" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should load new hints twice" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Cached hints should not change due to allowed hint kinds settings update" + ); + assert_eq!( + vec!["other hint".to_string(), "type hint".to_string()], + visible_hint_labels(editor, cx) + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.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" + ); + }); + + for (new_allowed_hint_kinds, expected_visible_hints) in [ + (HashSet::from_iter([None]), vec!["other hint".to_string()]), + ( + HashSet::from_iter([Some(InlayHintKind::Type)]), + vec!["type hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Type)]), + vec!["other hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([None, Some(InlayHintKind::Parameter)]), + vec!["other hint".to_string(), "parameter hint".to_string()], + ), + ( + HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]), + vec!["parameter hint".to_string(), "type hint".to_string()], + ), + ( + HashSet::from_iter([ + None, + Some(InlayHintKind::Type), + Some(InlayHintKind::Parameter), + ]), + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + ), + ] { + edits_made += 1; + update_test_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: new_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: new_allowed_hint_kinds.contains(&None), + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + expected_visible_hints, + visible_hint_labels(editor, cx), + "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds, + "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change" + ); + }); + } + + edits_made += 1; + let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); + update_test_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: false, + show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: another_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: another_allowed_hint_kinds.contains(&None), + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when hints got disabled" + ); + assert!( + cached_hint_labels(editor).is_empty(), + "Should clear the cache when hints got disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "Should clear visible hints when hints got disabled" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds, + "Should update its allowed hint kinds even when hints got disabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "The editor should update the cache version after hints got disabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should not load new hints when they got disabled" + ); + 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" + ); + }); + + let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); + edits_made += 1; + update_test_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), + show_parameter_hints: final_allowed_hint_kinds + .contains(&Some(InlayHintKind::Parameter)), + show_other_hints: final_allowed_hint_kinds.contains(&None), + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 3, + "Should query for new hints when they got reenabled" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + "Should get its cached hints fully repopulated after the hints got reenabled" + ); + assert_eq!( + vec!["parameter hint".to_string()], + visible_hint_labels(editor, cx), + "Should get its visible hints repopulated and filtered after the h" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!( + inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds, + "Cache should update editor settings when hints got reenabled" + ); + assert_eq!( + inlay_cache.version, edits_made, + "Cache should update its version after hints got reenabled" + ); + }); + + fake_server + .request::(()) + .await + .expect("inlay refresh request failed"); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 4, + "Should query for new hints again" + ); + assert_eq!( + vec![ + "other hint".to_string(), + "parameter hint".to_string(), + "type hint".to_string(), + ], + cached_hint_labels(editor), + ); + assert_eq!( + vec!["parameter hint".to_string()], + 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), + }) + }); + + let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await; + let fake_server = Arc::new(fake_server); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let another_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&another_lsp_request_count); + async move { + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path(file_with_hints).unwrap(), + ); + 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; + + let mut expected_changes = Vec::new(); + for change_after_opening in [ + "initial change #1", + "initial change #2", + "initial change #3", + ] { + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(change_after_opening, cx); + }); + expected_changes.push(change_after_opening); + } + + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::Relaxed), + 2, + "Should query new hints twice: for editor init and for the last edit that interrupted all others" + ); + let expected_hints = vec!["2".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!( + inlay_cache.version, 1, + "Only one update should be registered in the cache after all cancellations" + ); + }); + + let mut edits = Vec::new(); + for async_later_change in [ + "another change #1", + "another change #2", + "another change #3", + ] { + expected_changes.push(async_later_change); + let task_editor = editor.clone(); + let mut task_cx = cx.clone(); + edits.push(cx.foreground().spawn(async move { + task_editor.update(&mut task_cx, |editor, cx| { + editor.change_selections(None, cx, |s| s.select_ranges([13..13])); + editor.handle_input(async_later_change, cx); + }); + })); + } + let _ = futures::future::join_all(edits).await; + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let current_text = editor.text(cx); + for change in &expected_changes { + assert!( + current_text.contains(change), + "Should apply all changes made" + ); + } + assert_eq!( + lsp_request_count.load(Ordering::SeqCst), + 3, + "Should query new hints one more time, for the last edit only" + ); + let expected_hints = vec!["3".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Should get hints from the last edit landed only" + ); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!( + inlay_cache.version, 2, + "Should update the cache version once more, for the new change" + ); + }); + } + + #[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), + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)), + "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 = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges); + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + + task_lsp_request_ranges.lock().push(params.range); + let query_start = params.range.start; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1; + Ok(Some(vec![lsp::InlayHint { + position: query_start, + 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(); + editor.update(cx, |editor, cx| { + let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); + ranges.sort_by_key(|range| range.start); + assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); + assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); + assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line"); + assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent"); + + assert_eq!(lsp_request_count.load(Ordering::SeqCst), 2, + "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); + let expected_layers = vec!["1".to_string(), "2".to_string()]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "Should have hints from both LSP requests made for a big file" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!( + inlay_cache.version, 2, + "Both LSP queries should've bumped the cache version" + ); + }); + + editor.update(cx, |editor, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + editor.scroll_screen(&ScrollAmount::Page(1.0), cx); + editor.change_selections(None, cx, |s| s.select_ranges([600..600])); + editor.handle_input("++++more text++++", cx); + }); + + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); + ranges.sort_by_key(|range| range.start); + assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints"); + assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); + assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end"); + assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning"); + assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning"); + assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line"); + assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent"); + + assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5, + "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints"); + let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor), + "Should have hints from the new LSP response after edit"); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added"); + }); + } + + #[gpui::test] + async fn test_multiple_excerpts_large_multibuffer( + deterministic: Arc, + 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), + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + multibuffer.push_excerpts( + buffer_1.clone(), + [ + ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 0)..Point::new(11, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 0)..Point::new(33, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 0)..Point::new(55, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 0)..Point::new(66, 0), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 0)..Point::new(77, 0), + primary: None, + }, + ], + cx, + ); + multibuffer.push_excerpts( + buffer_2.clone(), + [ + ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(4, 1)..Point::new(11, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(22, 1)..Point::new(33, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(44, 1)..Point::new(55, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(56, 1)..Point::new(66, 1), + primary: None, + }, + ExcerptRange { + context: Point::new(67, 1)..Point::new(77, 1), + primary: None, + }, + ], + cx, + ); + multibuffer + }); + + deterministic.run_until_parked(); + cx.foreground().run_until_parked(); + let (_, editor) = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + let expected_layers = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + ]; + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" + ); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison"); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) + }); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + ]; + assert_eq!(expected_layers, cached_hint_labels(editor), + "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!(inlay_cache.version, 9); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_layers, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!(inlay_cache.version, 12); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec![ + "main hint #0".to_string(), + "main hint #1".to_string(), + "main hint #2".to_string(), + "main hint #3".to_string(), + "main hint #4".to_string(), + "main hint #5".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_layers, cached_hint_labels(editor), + "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds); + assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer"); + }); + + editor_edited.store(true, Ordering::Release); + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_layers = vec![ + "main hint(edited) #0".to_string(), + "main hint(edited) #1".to_string(), + "main hint(edited) #2".to_string(), + "main hint(edited) #3".to_string(), + "other hint #0".to_string(), + "other hint #1".to_string(), + "other hint #2".to_string(), + "other hint #3".to_string(), + "other hint #4".to_string(), + "other hint #5".to_string(), + ]; + assert_eq!(expected_layers, cached_hint_labels(editor), + "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \ +unedited (2nd) buffer should have the same hint"); + assert_eq!(expected_layers, 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); + }); + } + + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { + cx.foreground().forbid_parking(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + theme::init((), cx); + client::init_settings(cx); + language::init(cx); + Project::init_settings(cx); + workspace::init_settings(cx); + crate::init(cx); + }); + + update_test_settings(cx, f); + } + + async fn prepare_test_objects( + cx: &mut TestAppContext, + ) -> (&'static str, ViewHandle, FakeLanguageServer) { + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + + let 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.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 = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + ("/a/main.rs", editor, fake_server) + } + + fn cached_hint_labels(editor: &Editor) -> Vec { + let mut labels = Vec::new(); + for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { + let excerpt_hints = excerpt_hints.read(); + for (_, inlay) in excerpt_hints.hints.iter() { + match &inlay.label { + project::InlayHintLabel::String(s) => labels.push(s.to_string()), + _ => unreachable!(), + } + } + } + + labels.sort(); + labels + } + + fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + let mut hints = editor + .visible_inlay_hints(cx) + .into_iter() + .map(|hint| hint.text.to_string()) + .collect::>(); + hints.sort(); + hints + } +} diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 955902da12..31af03f768 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1010,7 +1010,7 @@ impl MultiBuffer { let suffix = cursor.suffix(&()); let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.push_tree(suffix, &()); + new_excerpts.append(suffix, &()); drop(cursor); snapshot.excerpts = new_excerpts; snapshot.excerpt_ids = new_excerpt_ids; @@ -1193,7 +1193,7 @@ impl MultiBuffer { while let Some(excerpt_id) = excerpt_ids.next() { // Seek to the next excerpt to remove, preserving any preceding excerpts. let locator = snapshot.excerpt_locator_for_id(excerpt_id); - new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &()); + new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); if let Some(mut excerpt) = cursor.item() { if excerpt.id != excerpt_id { @@ -1245,7 +1245,7 @@ impl MultiBuffer { } let suffix = cursor.suffix(&()); let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.push_tree(suffix, &()); + new_excerpts.append(suffix, &()); drop(cursor); snapshot.excerpts = new_excerpts; @@ -1509,7 +1509,7 @@ impl MultiBuffer { let mut cursor = snapshot.excerpts.cursor::<(Option<&Locator>, usize)>(); for (locator, buffer, buffer_edited) in excerpts_to_edit { - new_excerpts.push_tree(cursor.slice(&Some(locator), Bias::Left, &()), &()); + new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); let old_excerpt = cursor.item().unwrap(); let buffer = buffer.read(cx); let buffer_id = buffer.remote_id(); @@ -1549,7 +1549,7 @@ impl MultiBuffer { new_excerpts.push(new_excerpt, &()); cursor.next(&()); } - new_excerpts.push_tree(cursor.suffix(&()), &()); + new_excerpts.append(cursor.suffix(&()), &()); drop(cursor); snapshot.excerpts = new_excerpts; diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index 9a5145c244..1be4dc2dfb 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -49,6 +49,10 @@ impl Anchor { } } + pub fn bias(&self) -> Bias { + self.text_anchor.bias + } + pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { if self.text_anchor.bias != Bias::Left { if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { @@ -81,6 +85,19 @@ impl Anchor { { snapshot.summary_for_anchor(self) } + + pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool { + if *self == Anchor::min() || *self == Anchor::max() { + true + } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { + excerpt.contains(self) + && (self.text_anchor == excerpt.range.context.start + || self.text_anchor == excerpt.range.context.end + || self.text_anchor.is_valid(&excerpt.buffer)) + } else { + false + } + } } impl ToOffset for Anchor { diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index a13619a82a..d595337428 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -13,13 +13,14 @@ use gpui::{ }; use language::{Bias, Point}; use util::ResultExt; -use workspace::WorkspaceId; +use workspace::{item::Item, WorkspaceId}; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, hover_popover::hide_hover, persistence::DB, - Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint, + Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot, + ToPoint, }; use self::{ @@ -293,8 +294,19 @@ impl Editor { self.scroll_manager.visible_line_count } - pub(crate) fn set_visible_line_count(&mut self, lines: f32) { - self.scroll_manager.visible_line_count = Some(lines) + pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext) { + let opened_first_time = self.scroll_manager.visible_line_count.is_none(); + self.scroll_manager.visible_line_count = Some(lines); + if opened_first_time { + cx.spawn(|editor, mut cx| async move { + editor + .update(&mut cx, |editor, cx| { + editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx) + }) + .ok() + }) + .detach() + } } pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { @@ -320,6 +332,10 @@ impl Editor { workspace_id, cx, ); + + if !self.is_singleton(cx) { + self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); + } } pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { @@ -368,7 +384,7 @@ impl Editor { } let cur_position = self.scroll_position(cx); - let new_pos = cur_position + vec2f(0., amount.lines(self) - 1.); + let new_pos = cur_position + vec2f(0., amount.lines(self)); self.set_scroll_position(new_pos, cx); } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index da5e3603e7..82c2e10589 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -27,22 +27,22 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::scroll_cursor_center); cx.add_action(Editor::scroll_cursor_bottom); cx.add_action(|this: &mut Editor, _: &LineDown, cx| { - this.scroll_screen(&ScrollAmount::LineDown, cx) + this.scroll_screen(&ScrollAmount::Line(1.), cx) }); cx.add_action(|this: &mut Editor, _: &LineUp, cx| { - this.scroll_screen(&ScrollAmount::LineUp, cx) + this.scroll_screen(&ScrollAmount::Line(-1.), cx) }); cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| { - this.scroll_screen(&ScrollAmount::HalfPageDown, cx) + this.scroll_screen(&ScrollAmount::Page(0.5), cx) }); cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| { - this.scroll_screen(&ScrollAmount::HalfPageUp, cx) + this.scroll_screen(&ScrollAmount::Page(-0.5), cx) }); cx.add_action(|this: &mut Editor, _: &PageDown, cx| { - this.scroll_screen(&ScrollAmount::PageDown, cx) + this.scroll_screen(&ScrollAmount::Page(1.), cx) }); cx.add_action(|this: &mut Editor, _: &PageUp, cx| { - this.scroll_screen(&ScrollAmount::PageUp, cx) + this.scroll_screen(&ScrollAmount::Page(-1.), cx) }); } diff --git a/crates/editor/src/scroll/scroll_amount.rs b/crates/editor/src/scroll/scroll_amount.rs index 6f6c21f0d4..f9d09adcf5 100644 --- a/crates/editor/src/scroll/scroll_amount.rs +++ b/crates/editor/src/scroll/scroll_amount.rs @@ -6,12 +6,10 @@ use crate::Editor; #[derive(Clone, PartialEq, Deserialize)] pub enum ScrollAmount { - LineUp, - LineDown, - HalfPageUp, - HalfPageDown, - PageUp, - PageDown, + // Scroll N lines (positive is towards the end of the document) + Line(f32), + // Scroll N pages (positive is towards the end of the document) + Page(f32), } impl ScrollAmount { @@ -24,10 +22,10 @@ impl ScrollAmount { let context_menu = editor.context_menu.as_mut()?; match self { - Self::LineDown | Self::HalfPageDown => context_menu.select_next(cx), - Self::LineUp | Self::HalfPageUp => context_menu.select_prev(cx), - Self::PageDown => context_menu.select_last(cx), - Self::PageUp => context_menu.select_first(cx), + Self::Line(c) if *c > 0. => context_menu.select_next(cx), + Self::Line(_) => context_menu.select_prev(cx), + Self::Page(c) if *c > 0. => context_menu.select_last(cx), + Self::Page(_) => context_menu.select_first(cx), } .then_some(()) }) @@ -36,13 +34,13 @@ impl ScrollAmount { pub fn lines(&self, editor: &mut Editor) -> f32 { match self { - Self::LineDown => 1., - Self::LineUp => -1., - Self::HalfPageDown => editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.), - Self::HalfPageUp => -editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.), - // Minus 1. here so that there is a pivot line that stays on the screen - Self::PageDown => editor.visible_line_count().unwrap_or(1.) - 1., - Self::PageUp => -editor.visible_line_count().unwrap_or(1.) - 1., + Self::Line(count) => *count, + Self::Page(count) => editor + .visible_line_count() + // subtract one to leave an anchor line + // round towards zero (so page-up and page-down are symmetric) + .map(|l| ((l - 1.) * count).trunc()) + .unwrap_or(0.), } } } diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index d32a3e5b4c..beb5284031 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -41,7 +41,8 @@ impl View for DeployFeedbackButton { .status_bar .panel_buttons .button - .style_for(state, active); + .in_state(active) + .style_for(state); Svg::new("icons/feedback_16.svg") .with_color(style.icon_color) diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index 56bc235570..15f77bd561 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/crates/feedback/src/submit_feedback_button.rs @@ -48,7 +48,7 @@ impl View for SubmitFeedbackButton { let theme = theme::current(cx).clone(); enum SubmitFeedbackButton {} MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.feedback.submit_button.style_for(state, false); + let style = theme.feedback.submit_button.style_for(state); Label::new("Submit as Markdown", style.text.clone()) .contained() .with_style(style.container) diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 73e7ca6eaa..3f6bd83760 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -546,7 +546,7 @@ impl PickerDelegate for FileFinderDelegate { .get(ix) .expect("Invalid matches state: no element for index {ix}"); let theme = theme::current(cx); - let style = theme.picker.item.style_for(mouse_state, selected); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); Flex::column() diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 7dda3f7273..b3ebd224b0 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -31,6 +31,10 @@ 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"] } [features] test-support = [] diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index fee7765d49..ec8a249ff4 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -108,6 +108,7 @@ pub trait Fs: Send + Sync { async fn canonicalize(&self, path: &Path) -> Result; async fn is_file(&self, path: &Path) -> bool; async fn metadata(&self, path: &Path) -> Result>; + async fn read_link(&self, path: &Path) -> Result; async fn read_dir( &self, path: &Path, @@ -278,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) { @@ -323,6 +327,11 @@ impl Fs for RealFs { })) } + async fn read_link(&self, path: &Path) -> Result { + let path = smol::fs::read_link(path).await?; + Ok(path) + } + async fn read_dir( &self, path: &Path, @@ -382,6 +391,8 @@ struct FakeFsState { event_txs: Vec>>, events_paused: bool, buffered_events: Vec, + metadata_call_count: usize, + read_dir_call_count: usize, } #[cfg(any(test, feature = "test-support"))] @@ -407,46 +418,51 @@ enum FakeFsEntry { impl FakeFsState { fn read_path<'a>(&'a self, target: &Path) -> Result>> { Ok(self - .try_read_path(target) + .try_read_path(target, true) .ok_or_else(|| anyhow!("path does not exist: {}", target.display()))? .0) } - fn try_read_path<'a>(&'a self, target: &Path) -> Option<(Arc>, PathBuf)> { + fn try_read_path<'a>( + &'a self, + target: &Path, + follow_symlink: bool, + ) -> Option<(Arc>, PathBuf)> { let mut path = target.to_path_buf(); - let mut real_path = PathBuf::new(); + let mut canonical_path = PathBuf::new(); let mut entry_stack = Vec::new(); 'outer: loop { - let mut path_components = path.components().collect::>(); - while let Some(component) = path_components.pop_front() { + let mut path_components = path.components().peekable(); + while let Some(component) = path_components.next() { match component { Component::Prefix(_) => panic!("prefix paths aren't supported"), Component::RootDir => { entry_stack.clear(); entry_stack.push(self.root.clone()); - real_path.clear(); - real_path.push("/"); + canonical_path.clear(); + canonical_path.push("/"); } Component::CurDir => {} Component::ParentDir => { entry_stack.pop()?; - real_path.pop(); + canonical_path.pop(); } Component::Normal(name) => { let current_entry = entry_stack.last().cloned()?; let current_entry = current_entry.lock(); if let FakeFsEntry::Dir { entries, .. } = &*current_entry { let entry = entries.get(name.to_str().unwrap()).cloned()?; - let _entry = entry.lock(); - if let FakeFsEntry::Symlink { target, .. } = &*_entry { - let mut target = target.clone(); - target.extend(path_components); - path = target; - continue 'outer; - } else { - entry_stack.push(entry.clone()); - real_path.push(name); + if path_components.peek().is_some() || follow_symlink { + let entry = entry.lock(); + if let FakeFsEntry::Symlink { target, .. } = &*entry { + let mut target = target.clone(); + target.extend(path_components); + path = target; + continue 'outer; + } } + entry_stack.push(entry.clone()); + canonical_path.push(name); } else { return None; } @@ -455,7 +471,7 @@ impl FakeFsState { } break; } - entry_stack.pop().map(|entry| (entry, real_path)) + Some((entry_stack.pop()?, canonical_path)) } fn write_path(&self, path: &Path, callback: Fn) -> Result @@ -525,6 +541,8 @@ impl FakeFs { event_txs: Default::default(), buffered_events: Vec::new(), events_paused: false, + read_dir_call_count: 0, + metadata_call_count: 0, }), }) } @@ -761,6 +779,16 @@ impl FakeFs { result } + /// How many `read_dir` calls have been issued. + pub fn read_dir_call_count(&self) -> usize { + self.state.lock().read_dir_call_count + } + + /// How many `metadata` calls have been issued. + pub fn metadata_call_count(&self) -> usize { + self.state.lock().metadata_call_count + } + async fn simulate_random_delay(&self) { self.executor .upgrade() @@ -776,6 +804,10 @@ impl FakeFsEntry { matches!(self, Self::File { .. }) } + fn is_symlink(&self) -> bool { + matches!(self, Self::Symlink { .. }) + } + fn file_content(&self, path: &Path) -> Result<&String> { if let Self::File { content, .. } = self { Ok(content) @@ -1048,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(()) } @@ -1056,8 +1091,8 @@ impl Fs for FakeFs { let path = normalize_path(path); self.simulate_random_delay().await; let state = self.state.lock(); - if let Some((_, real_path)) = state.try_read_path(&path) { - Ok(real_path) + if let Some((_, canonical_path)) = state.try_read_path(&path, true) { + Ok(canonical_path) } else { Err(anyhow!("path does not exist: {}", path.display())) } @@ -1067,7 +1102,7 @@ impl Fs for FakeFs { let path = normalize_path(path); self.simulate_random_delay().await; let state = self.state.lock(); - if let Some((entry, _)) = state.try_read_path(&path) { + if let Some((entry, _)) = state.try_read_path(&path, true) { entry.lock().is_file() } else { false @@ -1077,11 +1112,19 @@ impl Fs for FakeFs { async fn metadata(&self, path: &Path) -> Result> { self.simulate_random_delay().await; let path = normalize_path(path); - let state = self.state.lock(); - if let Some((entry, real_path)) = state.try_read_path(&path) { - let entry = entry.lock(); - let is_symlink = real_path != path; + let mut state = self.state.lock(); + state.metadata_call_count += 1; + if let Some((mut entry, _)) = state.try_read_path(&path, false) { + let is_symlink = entry.lock().is_symlink(); + if is_symlink { + if let Some(e) = state.try_read_path(&path, true).map(|e| e.0) { + entry = e; + } else { + return Ok(None); + } + } + let entry = entry.lock(); Ok(Some(match &*entry { FakeFsEntry::File { inode, mtime, .. } => Metadata { inode: *inode, @@ -1102,13 +1145,30 @@ impl Fs for FakeFs { } } + async fn read_link(&self, path: &Path) -> Result { + self.simulate_random_delay().await; + let path = normalize_path(path); + let state = self.state.lock(); + if let Some((entry, _)) = state.try_read_path(&path, false) { + let entry = entry.lock(); + if let FakeFsEntry::Symlink { target } = &*entry { + Ok(target.clone()) + } else { + Err(anyhow!("not a symlink: {}", path.display())) + } + } else { + Err(anyhow!("path does not exist: {}", path.display())) + } + } + async fn read_dir( &self, path: &Path, ) -> Result>>>> { self.simulate_random_delay().await; let path = normalize_path(path); - let state = self.state.lock(); + let mut state = self.state.lock(); + state.read_dir_call_count += 1; let entry = state.read_path(&path)?; let mut entry = entry.lock(); let children = entry.dir_entries(&path)?; diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 488262887f..0e5fd8343f 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -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, + /// Timestamp of most recent commit, normalized to Unix Epoch format. + pub unix_timestamp: Option, +} #[async_trait::async_trait] pub trait GitRepository: Send { fn reload_index(&self); @@ -27,6 +33,12 @@ pub trait GitRepository: Send { fn statuses(&self) -> Option>; fn status(&self, path: &RepoPath) -> Result>; + fn branches(&self) -> Result> { + 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> { + 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 { diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 0b41ee6dca..769f2eda55 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -24,6 +24,7 @@ pub struct GoToLine { prev_scroll_position: Option, 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.has_focus = true; cx.focus(&self.line_editor); } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + 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) } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 882800f128..640614324f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -152,6 +152,29 @@ impl App { asset_source, )))); + foreground_platform.on_event(Box::new({ + let cx = app.0.clone(); + move |event| { + if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event { + // Allow system menu "cmd-?" shortcut to be overridden + if keystroke.cmd + && !keystroke.shift + && !keystroke.alt + && !keystroke.function + && keystroke.key == "?" + { + if cx + .borrow_mut() + .update_active_window(|cx| cx.dispatch_keystroke(keystroke)) + .unwrap_or(false) + { + return true; + } + } + } + false + } + })); foreground_platform.on_quit(Box::new({ let cx = app.0.clone(); move || { @@ -445,7 +468,7 @@ type WindowBoundsCallback = Box>, &mut WindowContext) -> bool>; type ActiveLabeledTasksCallback = Box bool>; -type DeserializeActionCallback = fn(json: &str) -> anyhow::Result>; +type DeserializeActionCallback = fn(json: serde_json::Value) -> anyhow::Result>; type WindowShouldCloseSubscriptionCallback = Box bool>; pub struct AppContext { @@ -624,14 +647,14 @@ impl AppContext { pub fn deserialize_action( &self, name: &str, - argument: Option<&str>, + argument: Option, ) -> Result> { let callback = self .action_deserializers .get(name) .ok_or_else(|| anyhow!("unknown action {}", name))? .1; - callback(argument.unwrap_or("{}")) + callback(argument.unwrap_or_else(|| serde_json::Value::Object(Default::default()))) .with_context(|| format!("invalid data for action {}", name)) } @@ -2948,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 { @@ -2974,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(&mut self, mut callback: F) @@ -3281,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { let region_id = MouseRegionId::new::(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, } @@ -5573,7 +5597,7 @@ mod tests { let action1 = cx .deserialize_action( "test::something::ComplexAction", - Some(r#"{"arg": "a", "count": 5}"#), + Some(serde_json::from_str(r#"{"arg": "a", "count": 5}"#).unwrap()), ) .unwrap(); let action2 = cx diff --git a/crates/gpui/src/app/action.rs b/crates/gpui/src/app/action.rs index 5b4df68a65..c6b43e489b 100644 --- a/crates/gpui/src/app/action.rs +++ b/crates/gpui/src/app/action.rs @@ -11,7 +11,7 @@ pub trait Action: 'static { fn qualified_name() -> &'static str where Self: Sized; - fn from_json_str(json: &str) -> anyhow::Result> + fn from_json_str(json: serde_json::Value) -> anyhow::Result> where Self: Sized; } @@ -38,7 +38,7 @@ macro_rules! actions { $crate::__impl_action! { $namespace, $name, - fn from_json_str(_: &str) -> $crate::anyhow::Result> { + fn from_json_str(_: $crate::serde_json::Value) -> $crate::anyhow::Result> { Ok(Box::new(Self)) } } @@ -58,8 +58,8 @@ macro_rules! impl_actions { $crate::__impl_action! { $namespace, $name, - fn from_json_str(json: &str) -> $crate::anyhow::Result> { - Ok(Box::new($crate::serde_json::from_str::(json)?)) + fn from_json_str(json: $crate::serde_json::Value) -> $crate::anyhow::Result> { + Ok(Box::new($crate::serde_json::from_value::(json)?)) } } )* diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index cfcef626df..58d7bb4c40 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -8,14 +8,14 @@ use crate::{ MouseButton, MouseMovedEvent, PromptLevel, WindowBounds, }, scene::{ - CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, - MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene, + CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent, + MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene, }, 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, pub(crate) hovered_region_ids: HashSet, pub(crate) clicked_region_ids: HashSet, - pub(crate) clicked_button: Option, + 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, @@ -394,7 +394,7 @@ impl<'a> WindowContext<'a> { .iter() .filter_map(move |(name, (type_id, deserialize))| { if let Some(action_depth) = handler_depths_by_action_type.get(type_id).copied() { - let action = deserialize("{}").ok()?; + let action = deserialize(serde_json::Value::Object(Default::default())).ok()?; let bindings = self .keystroke_matcher .bindings_for_action_type(*type_id) @@ -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 { @@ -524,6 +538,10 @@ impl<'a> WindowContext<'a> { region: Default::default(), platform_event: e.clone(), })); + mouse_events.push(MouseEvent::ClickOut(MouseClickOut { + region: Default::default(), + platform_event: e.clone(), + })); } Event::MouseMoved( @@ -556,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 { @@ -679,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 @@ -688,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() { @@ -712,7 +730,10 @@ impl<'a> WindowContext<'a> { } } - MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => { + MouseEvent::MoveOut(_) + | MouseEvent::UpOut(_) + | MouseEvent::DownOut(_) + | MouseEvent::ClickOut(_) => { for (mouse_region, _) in self.window.mouse_regions.iter().rev() { // NOT contains if !mouse_region @@ -860,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, }) @@ -1085,6 +1098,10 @@ impl<'a> WindowContext<'a> { self.window.focused_view_id } + pub fn focus(&mut self, view_id: Option) { + self.app_context.focus(self.window_id, view_id); + } + pub fn window_bounds(&self) -> WindowBounds { self.window.platform_window.bounds() } @@ -1176,8 +1193,6 @@ impl<'a> WindowContext<'a> { pub struct RenderParams { pub view_id: usize, pub titlebar_height: f32, - pub hovered_region_ids: HashSet, - pub clicked_region_ids: Option<(HashSet, MouseButton)>, pub refreshing: bool, pub appearance: Appearance, } diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index b6c1e3aff9..2a0c2c1dc1 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -6,15 +6,16 @@ use std::{ use crate::json::ToJson; use pathfinder_color::{ColorF, ColorU}; +use schemars::JsonSchema; use serde::{ de::{self, Unexpected}, Deserialize, Deserializer, }; use serde_json::json; -#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord)] +#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, PartialOrd, Ord, JsonSchema)] #[repr(transparent)] -pub struct Color(ColorU); +pub struct Color(#[schemars(with = "String")] ColorU); impl Color { pub fn transparent_black() -> Self { diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 779f4b6ec3..78403444ff 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -41,13 +41,7 @@ use collections::HashMap; use core::panic; use json::ToJson; use smallvec::SmallVec; -use std::{ - any::Any, - borrow::Cow, - marker::PhantomData, - mem, - ops::{Deref, DerefMut, Range}, -}; +use std::{any::Any, borrow::Cow, mem, ops::Range}; pub trait Element: 'static { type LayoutState; @@ -567,90 +561,6 @@ impl RootElement { } } -pub trait Component: 'static { - fn render(&self, view: &mut V, cx: &mut ViewContext) -> AnyElement; -} - -pub struct ComponentHost> { - component: C, - view_type: PhantomData, -} - -impl> ComponentHost { - pub fn new(c: C) -> Self { - Self { - component: c, - view_type: PhantomData, - } - } -} - -impl> Deref for ComponentHost { - type Target = C; - - fn deref(&self) -> &Self::Target { - &self.component - } -} - -impl> DerefMut for ComponentHost { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.component - } -} - -impl> Element for ComponentHost { - type LayoutState = AnyElement; - type PaintState = (); - - fn layout( - &mut self, - constraint: SizeConstraint, - view: &mut V, - cx: &mut LayoutContext, - ) -> (Vector2F, AnyElement) { - let mut element = self.component.render(view, cx); - let size = element.layout(constraint, view, cx); - (size, element) - } - - fn paint( - &mut self, - scene: &mut SceneBuilder, - bounds: RectF, - visible_bounds: RectF, - element: &mut AnyElement, - view: &mut V, - cx: &mut ViewContext, - ) { - element.paint(scene, bounds.origin(), visible_bounds, view, cx); - } - - fn rect_for_text_range( - &self, - range_utf16: Range, - _: RectF, - _: RectF, - element: &AnyElement, - _: &(), - view: &V, - cx: &ViewContext, - ) -> Option { - element.rect_for_text_range(range_utf16, view, cx) - } - - fn debug( - &self, - _: RectF, - element: &AnyElement, - _: &(), - view: &V, - cx: &ViewContext, - ) -> serde_json::Value { - element.debug(view, cx) - } -} - pub trait AnyRootElement { fn layout( &mut self, diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 3ce323db09..3b95feb9ef 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -12,10 +12,11 @@ use crate::{ scene::{self, Border, CursorRegion, Quad}, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, }; +use schemars::JsonSchema; use serde::Deserialize; use serde_json::json; -#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] pub struct ContainerStyle { #[serde(default)] pub margin: Margin, @@ -332,7 +333,7 @@ impl ToJson for ContainerStyle { } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, JsonSchema)] pub struct Margin { pub top: f32, pub left: f32, @@ -359,7 +360,7 @@ impl ToJson for Margin { } } -#[derive(Clone, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default, JsonSchema)] pub struct Padding { pub top: f32, pub left: f32, @@ -486,9 +487,10 @@ impl ToJson for Padding { } } -#[derive(Clone, Copy, Debug, Default, Deserialize)] +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] pub struct Shadow { #[serde(default, deserialize_with = "deserialize_vec2f")] + #[schemars(with = "Vec::")] offset: Vector2F, #[serde(default)] blur: f32, diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index 98c5ae6226..df200eae7f 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -8,6 +8,7 @@ use crate::{ scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, }; +use schemars::JsonSchema; use serde::Deserialize; use std::{ops::Range, sync::Arc}; @@ -21,7 +22,7 @@ pub struct Image { style: ImageStyle, } -#[derive(Copy, Clone, Default, Deserialize)] +#[derive(Copy, Clone, Default, Deserialize, JsonSchema)] pub struct ImageStyle { #[serde(default)] pub border: Border, diff --git a/crates/gpui/src/elements/label.rs b/crates/gpui/src/elements/label.rs index 9499841b3d..d9cf537333 100644 --- a/crates/gpui/src/elements/label.rs +++ b/crates/gpui/src/elements/label.rs @@ -10,6 +10,7 @@ use crate::{ text_layout::{Line, RunStyle}, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, }; +use schemars::JsonSchema; use serde::Deserialize; use serde_json::json; use smallvec::{smallvec, SmallVec}; @@ -20,7 +21,7 @@ pub struct Label { highlight_indices: Vec, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct LabelStyle { pub text: TextStyle, pub highlight_text: Option, @@ -164,6 +165,7 @@ impl Element for Label { _: &mut V, cx: &mut ViewContext, ) -> Self::PaintState { + let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); line.paint( scene, bounds.origin(), diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 1cf8cc986f..4c6298d8f5 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -211,7 +211,7 @@ impl Element for List { let mut cursor = old_items.cursor::(); if state.rendered_range.start < new_rendered_range.start { - new_items.push_tree( + new_items.append( cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()), &(), ); @@ -221,7 +221,7 @@ impl Element for List { cursor.next(&()); } } - new_items.push_tree( + new_items.append( cursor.slice(&Count(new_rendered_range.start), Bias::Right, &()), &(), ); @@ -230,7 +230,7 @@ impl Element for List { cursor.seek(&Count(new_rendered_range.end), Bias::Right, &()); if new_rendered_range.end < state.rendered_range.start { - new_items.push_tree( + new_items.append( cursor.slice(&Count(state.rendered_range.start), Bias::Right, &()), &(), ); @@ -240,7 +240,7 @@ impl Element for List { cursor.next(&()); } - new_items.push_tree(cursor.suffix(&()), &()); + new_items.append(cursor.suffix(&()), &()); state.items = new_items; state.rendered_range = new_rendered_range; @@ -413,7 +413,7 @@ impl ListState { old_heights.seek_forward(&Count(old_range.end), Bias::Right, &()); new_heights.extend((0..count).map(|_| ListItem::Unrendered), &()); - new_heights.push_tree(old_heights.suffix(&()), &()); + new_heights.append(old_heights.suffix(&()), &()); drop(old_heights); state.items = new_heights; } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 6f2762db66..1b8142d964 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -7,8 +7,8 @@ use crate::{ platform::CursorStyle, platform::MouseButton, scene::{ - CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover, - MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, + CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, + MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, }, AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder, SizeConstraint, View, ViewContext, @@ -136,6 +136,15 @@ impl MouseEventHandler { self } + pub fn on_click_out( + mut self, + button: MouseButton, + handler: impl Fn(MouseClickOut, &mut V, &mut EventContext) + 'static, + ) -> Self { + self.handlers = self.handlers.on_click_out(button, handler); + self + } + pub fn on_down_out( mut self, button: MouseButton, diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index 5444221322..9792f16cbe 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,7 +1,5 @@ -use std::{borrow::Cow, ops::Range}; - -use serde_json::json; - +use super::constrain_size_preserving_aspect_ratio; +use crate::json::ToJson; use crate::{ color::Color, geometry::{ @@ -10,6 +8,10 @@ use crate::{ }, scene, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, }; +use schemars::JsonSchema; +use serde_derive::Deserialize; +use serde_json::json; +use std::{borrow::Cow, ops::Range}; pub struct Svg { path: Cow<'static, str>, @@ -24,6 +26,14 @@ impl Svg { } } + pub fn for_style(style: SvgStyle) -> impl Element { + Self::new(style.asset) + .with_color(style.color) + .constrained() + .with_width(style.dimensions.width) + .with_height(style.dimensions.height) + } + pub fn with_color(mut self, color: Color) -> Self { self.color = color; self @@ -105,9 +115,24 @@ impl Element for Svg { } } -use crate::json::ToJson; +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct SvgStyle { + pub color: Color, + pub asset: String, + pub dimensions: Dimensions, +} -use super::constrain_size_preserving_aspect_ratio; +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct Dimensions { + pub width: f32, + pub height: f32, +} + +impl Dimensions { + pub fn to_vec(&self) -> Vector2F { + vec2f(self.width, self.height) + } +} fn from_usvg_rect(rect: usvg::Rect) -> RectF { RectF::new( diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 7b4892fc1c..f21b1c363c 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -9,6 +9,7 @@ use crate::{ Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View, ViewContext, }; +use schemars::JsonSchema; use serde::Deserialize; use std::{ cell::{Cell, RefCell}, @@ -33,7 +34,7 @@ struct TooltipState { debounce: RefCell>>, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct TooltipStyle { #[serde(flatten)] pub container: ContainerStyle, @@ -42,7 +43,7 @@ pub struct TooltipStyle { pub max_text_width: Option, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct KeystrokeStyle { #[serde(flatten)] container: ContainerStyle, diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 365766fb9d..712c854488 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -7,6 +7,7 @@ use std::{ fmt::{self, Display}, marker::PhantomData, mem, + panic::Location, pin::Pin, rc::Rc, sync::Arc, @@ -965,10 +966,12 @@ impl Task { } impl Task> { + #[track_caller] pub fn detach_and_log_err(self, cx: &mut AppContext) { + let caller = Location::caller(); cx.spawn(|_| async move { if let Err(err) = self.await { - log::error!("{:#}", err); + log::error!("{}:{}: {:#}", caller.file(), caller.line(), err); } }) .detach(); diff --git a/crates/gpui/src/font_cache.rs b/crates/gpui/src/font_cache.rs index 57dad48e34..4f0d4fd461 100644 --- a/crates/gpui/src/font_cache.rs +++ b/crates/gpui/src/font_cache.rs @@ -7,13 +7,14 @@ use crate::{ use anyhow::{anyhow, Result}; use ordered_float::OrderedFloat; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; +use schemars::JsonSchema; use std::{ collections::HashMap, ops::{Deref, DerefMut}, sync::Arc, }; -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)] pub struct FamilyId(usize); struct Family { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 5e77593d05..3b4a94dd0e 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -16,7 +16,7 @@ use serde::{de, Deserialize, Serialize}; use serde_json::Value; use std::{cell::RefCell, sync::Arc}; -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, JsonSchema)] pub struct FontId(pub usize); pub type GlyphId = u32; @@ -59,20 +59,44 @@ pub struct Features { pub zero: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, JsonSchema)] pub struct TextStyle { pub color: Color, pub font_family_name: Arc, pub font_family_id: FamilyId, pub font_id: FontId, pub font_size: f32, + #[schemars(with = "PropertiesDef")] pub font_properties: Properties, pub underline: Underline, } -#[derive(Copy, Clone, Debug, Default, PartialEq)] +#[derive(JsonSchema)] +#[serde(remote = "Properties")] +pub struct PropertiesDef { + /// The font style, as defined in CSS. + pub style: StyleDef, + /// The font weight, as defined in CSS. + pub weight: f32, + /// The font stretchiness, as defined in CSS. + pub stretch: f32, +} + +#[derive(JsonSchema)] +#[schemars(remote = "Style")] +pub enum StyleDef { + /// A face that is neither italic not obliqued. + Normal, + /// A form that is generally cursive in nature. + Italic, + /// A typically-sloped version of the regular face. + Oblique, +} + +#[derive(Copy, Clone, Debug, Default, PartialEq, JsonSchema)] pub struct HighlightStyle { pub color: Option, + #[schemars(with = "Option::")] pub weight: Option, pub italic: Option, pub underline: Option, @@ -81,9 +105,10 @@ pub struct HighlightStyle { impl Eq for HighlightStyle {} -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, JsonSchema)] pub struct Underline { pub color: Option, + #[schemars(with = "f32")] pub thickness: OrderedFloat, pub squiggly: bool, } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index a172667fb9..3442934b3a 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -26,8 +26,10 @@ pub mod color; pub mod json; pub mod keymap_matcher; pub mod platform; -pub use gpui_macros::test; +pub use gpui_macros::{test, Element}; pub use window::{Axis, SizeConstraint, Vector2FExt, WindowContext}; pub use anyhow; pub use serde_json; + +actions!(zed, [NoAction]); diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 7fc02b0548..67f8e52c04 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -25,6 +25,7 @@ use anyhow::{anyhow, bail, Result}; use async_task::Runnable; pub use event::*; use postage::oneshot; +use schemars::JsonSchema; use serde::Deserialize; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -282,7 +283,7 @@ pub enum PromptLevel { Critical, } -#[derive(Copy, Clone, Debug, Deserialize)] +#[derive(Copy, Clone, Debug, Deserialize, JsonSchema)] pub enum CursorStyle { Arrow, ResizeLeftRight, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 8b5b801ada..e1d80fe25c 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -786,7 +786,7 @@ impl platform::Platform for MacPlatform { fn set_cursor_style(&self, style: CursorStyle) { unsafe { - let cursor: id = match style { + let new_cursor: id = match style { CursorStyle::Arrow => msg_send![class!(NSCursor), arrowCursor], CursorStyle::ResizeLeftRight => { msg_send![class!(NSCursor), resizeLeftRightCursor] @@ -795,7 +795,11 @@ impl platform::Platform for MacPlatform { CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor], CursorStyle::IBeam => msg_send![class!(NSCursor), IBeamCursor], }; - let _: () = msg_send![cursor, set]; + + let old_cursor: id = msg_send![class!(NSCursor), currentCursor]; + if new_cursor != old_cursor { + let _: () = msg_send![new_cursor, set]; + } } } @@ -935,7 +939,6 @@ extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { } } } - msg_send![super(this, class!(NSApplication)), sendEvent: native_event] } } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 96edee1757..7793a28ee0 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -3,6 +3,7 @@ mod mouse_region; #[cfg(debug_assertions)] use collections::HashSet; +use schemars::JsonSchema; use serde::Deserialize; use serde_json::json; use std::{borrow::Cow, sync::Arc}; @@ -99,7 +100,7 @@ pub struct Icon { pub color: Color, } -#[derive(Clone, Copy, Default, Debug)] +#[derive(Clone, Copy, Default, Debug, JsonSchema)] pub struct Border { pub width: f32, pub color: Color, diff --git a/crates/gpui/src/scene/mouse_event.rs b/crates/gpui/src/scene/mouse_event.rs index cf0a08f33e..a492da771b 100644 --- a/crates/gpui/src/scene/mouse_event.rs +++ b/crates/gpui/src/scene/mouse_event.rs @@ -99,6 +99,20 @@ impl Deref for MouseClick { } } +#[derive(Debug, Default, Clone)] +pub struct MouseClickOut { + pub region: RectF, + pub platform_event: MouseButtonEvent, +} + +impl Deref for MouseClickOut { + type Target = MouseButtonEvent; + + fn deref(&self) -> &Self::Target { + &self.platform_event + } +} + #[derive(Debug, Default, Clone)] pub struct MouseDownOut { pub region: RectF, @@ -150,6 +164,7 @@ pub enum MouseEvent { Down(MouseDown), Up(MouseUp), Click(MouseClick), + ClickOut(MouseClickOut), DownOut(MouseDownOut), UpOut(MouseUpOut), ScrollWheel(MouseScrollWheel), @@ -165,6 +180,7 @@ impl MouseEvent { MouseEvent::Down(r) => r.region = region, MouseEvent::Up(r) => r.region = region, MouseEvent::Click(r) => r.region = region, + MouseEvent::ClickOut(r) => r.region = region, MouseEvent::DownOut(r) => r.region = region, MouseEvent::UpOut(r) => r.region = region, MouseEvent::ScrollWheel(r) => r.region = region, @@ -182,6 +198,7 @@ impl MouseEvent { MouseEvent::Down(_) => true, MouseEvent::Up(_) => true, MouseEvent::Click(_) => true, + MouseEvent::ClickOut(_) => true, MouseEvent::DownOut(_) => false, MouseEvent::UpOut(_) => false, MouseEvent::ScrollWheel(_) => true, @@ -222,6 +239,10 @@ impl MouseEvent { discriminant(&MouseEvent::Click(Default::default())) } + pub fn click_out_disc() -> Discriminant { + discriminant(&MouseEvent::ClickOut(Default::default())) + } + pub fn down_out_disc() -> Discriminant { discriminant(&MouseEvent::DownOut(Default::default())) } @@ -239,6 +260,7 @@ impl MouseEvent { MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)), MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)), MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)), + MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)), MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)), MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)), MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None), diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index 0efc794148..ca2cc04b9d 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -14,7 +14,7 @@ use super::{ MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp, MouseUpOut, }, - MouseMoveOut, MouseScrollWheel, + MouseClickOut, MouseMoveOut, MouseScrollWheel, }; #[derive(Clone)] @@ -89,6 +89,15 @@ impl MouseRegion { self } + pub fn on_click_out(mut self, button: MouseButton, handler: F) -> Self + where + V: View, + F: Fn(MouseClickOut, &mut V, &mut EventContext) + 'static, + { + self.handlers = self.handlers.on_click_out(button, handler); + self + } + pub fn on_down_out(mut self, button: MouseButton, handler: F) -> Self where V: View, @@ -246,6 +255,10 @@ impl HandlerSet { HandlerKey::new(MouseEvent::click_disc(), Some(button)), SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), ); + set.insert( + HandlerKey::new(MouseEvent::click_out_disc(), Some(button)), + SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), + ); set.insert( HandlerKey::new(MouseEvent::down_out_disc(), Some(button)), SmallVec::from_buf([Rc::new(|_, _, _, _| true)]), @@ -405,6 +418,28 @@ impl HandlerSet { self } + pub fn on_click_out(mut self, button: MouseButton, handler: F) -> Self + where + V: View, + F: Fn(MouseClickOut, &mut V, &mut EventContext) + 'static, + { + self.insert(MouseEvent::click_out_disc(), Some(button), + Rc::new(move |region_event, view, cx, view_id| { + if let MouseEvent::ClickOut(e) = region_event { + let view = view.downcast_mut().unwrap(); + let mut cx = ViewContext::mutable(cx, view_id); + let mut cx = EventContext::new(&mut cx); + handler(e, view, &mut cx); + cx.handled + } else { + panic!( + "Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}", + region_event); + } + })); + self + } + pub fn on_down_out(mut self, button: MouseButton, handler: F) -> Self where V: View, diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index e976245e06..dbf57b83e5 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -3,8 +3,8 @@ use proc_macro2::Ident; use quote::{format_ident, quote}; use std::mem; use syn::{ - parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, FnArg, ItemFn, Lit, Meta, - NestedMeta, Type, + parse_macro_input, parse_quote, spanned::Spanned as _, AttributeArgs, DeriveInput, FnArg, + ItemFn, Lit, Meta, NestedMeta, Type, }; #[proc_macro_attribute] @@ -275,3 +275,68 @@ fn parse_bool(literal: &Lit) -> Result { result.map_err(|err| TokenStream::from(err.into_compile_error())) } + +#[proc_macro_derive(Element)] +pub fn element_derive(input: TokenStream) -> TokenStream { + // Parse the input tokens into a syntax tree + let input = parse_macro_input!(input as DeriveInput); + + // The name of the struct/enum + let name = input.ident; + + let expanded = quote! { + impl gpui::elements::Element for #name { + type LayoutState = gpui::elements::AnyElement; + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + view: &mut V, + cx: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, gpui::elements::AnyElement) { + let mut element = self.render(view, cx); + let size = element.layout(constraint, view, cx); + (size, element) + } + + fn paint( + &mut self, + scene: &mut gpui::SceneBuilder, + bounds: gpui::geometry::rect::RectF, + visible_bounds: gpui::geometry::rect::RectF, + element: &mut gpui::elements::AnyElement, + view: &mut V, + cx: &mut gpui::ViewContext, + ) { + element.paint(scene, bounds.origin(), visible_bounds, view, cx); + } + + fn rect_for_text_range( + &self, + range_utf16: std::ops::Range, + _: gpui::geometry::rect::RectF, + _: gpui::geometry::rect::RectF, + element: &gpui::elements::AnyElement, + _: &(), + view: &V, + cx: &gpui::ViewContext, + ) -> Option { + element.rect_for_text_range(range_utf16, view, cx) + } + + fn debug( + &self, + _: gpui::geometry::rect::RectF, + element: &gpui::elements::AnyElement, + _: &(), + view: &V, + cx: &gpui::ViewContext, + ) -> serde_json::Value { + element.debug(view, cx) + } + } + }; + // Return generated code + TokenStream::from(expanded) +} diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e91d5770cf..e8450344b8 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -17,10 +17,10 @@ use futures::{ future::{BoxFuture, Shared}, FutureExt, TryFutureExt as _, }; -use gpui::{executor::Background, AppContext, Task}; +use gpui::{executor::Background, AppContext, AsyncAppContext, Task}; use highlight_map::HighlightMap; use lazy_static::lazy_static; -use lsp::CodeActionKind; +use lsp::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; use postage::watch; use regex::Regex; @@ -30,7 +30,6 @@ use std::{ any::Any, borrow::Cow, cell::RefCell, - ffi::OsString, fmt::Debug, hash::Hash, mem, @@ -86,12 +85,6 @@ pub trait ToLspPosition { #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct LanguageServerName(pub Arc); -#[derive(Debug, Clone, Deserialize)] -pub struct LanguageServerBinary { - pub path: PathBuf, - pub arguments: Vec, -} - /// Represents a Language Server, with certain cached sync properties. /// Uses [`LspAdapter`] under the hood, but calls all 'static' methods /// once at startup, and caches the results. @@ -125,27 +118,57 @@ impl CachedLspAdapter { pub async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result> { - self.adapter.fetch_latest_server_version(http).await + self.adapter.fetch_latest_server_version(delegate).await + } + + pub fn will_fetch_server( + &self, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Option>> { + self.adapter.will_fetch_server(delegate, cx) + } + + pub fn will_start_server( + &self, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Option>> { + self.adapter.will_start_server(delegate, cx) } pub async fn fetch_server_binary( &self, version: Box, - http: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result { self.adapter - .fetch_server_binary(version, http, container_dir) + .fetch_server_binary(version, container_dir, delegate) .await } pub async fn cached_server_binary( &self, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Option { - self.adapter.cached_server_binary(container_dir).await + self.adapter + .cached_server_binary(container_dir, delegate) + .await + } + + pub fn can_be_reinstalled(&self) -> bool { + self.adapter.can_be_reinstalled() + } + + pub async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + self.adapter.installation_test_binary(container_dir).await } pub fn code_action_kinds(&self) -> Option> { @@ -187,23 +210,57 @@ impl CachedLspAdapter { } } +pub trait LspAdapterDelegate: Send + Sync { + fn show_notification(&self, message: &str, cx: &mut AppContext); + fn http_client(&self) -> Arc; +} + #[async_trait] pub trait LspAdapter: 'static + Send + Sync { async fn name(&self) -> LanguageServerName; async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result>; + fn will_fetch_server( + &self, + _: &Arc, + _: &mut AsyncAppContext, + ) -> Option>> { + None + } + + fn will_start_server( + &self, + _: &Arc, + _: &mut AsyncAppContext, + ) -> Option>> { + None + } + async fn fetch_server_binary( &self, version: Box, - http: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result; - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option; + async fn cached_server_binary( + &self, + container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, + ) -> Option; + + fn can_be_reinstalled(&self) -> bool { + true + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option; async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} @@ -513,10 +570,7 @@ pub struct LanguageRegistry { login_shell_env_loaded: Shared>, #[allow(clippy::type_complexity)] lsp_binary_paths: Mutex< - HashMap< - LanguageServerName, - Shared>>>, - >, + HashMap>>>>, >, executor: Option>, } @@ -535,7 +589,8 @@ struct LanguageRegistryState { pub struct PendingLanguageServer { pub server_id: LanguageServerId, - pub task: Task>, + pub task: Task>>, + pub container_dir: Option>, } impl LanguageRegistry { @@ -807,17 +862,17 @@ impl LanguageRegistry { self.state.read().languages.iter().cloned().collect() } - pub fn start_language_server( + pub fn create_pending_language_server( self: &Arc, language: Arc, adapter: Arc, root_path: Arc, - http_client: Arc, + delegate: Arc, cx: &mut AppContext, ) -> Option { let server_id = self.state.write().next_language_server_id(); log::info!( - "starting language server name:{}, path:{root_path:?}, id:{server_id}", + "starting language server {:?}, path: {root_path:?}, id: {server_id}", adapter.name.0 ); @@ -847,61 +902,81 @@ impl LanguageRegistry { } }) .detach(); - Ok(server) + + Ok(Some(server)) }); - return Some(PendingLanguageServer { server_id, task }); + return Some(PendingLanguageServer { + server_id, + task, + container_dir: None, + }); } let download_dir = self .language_server_download_dir .clone() - .ok_or_else(|| anyhow!("language server download directory has not been assigned")) + .ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server")) .log_err()?; let this = self.clone(); let language = language.clone(); - let http_client = http_client.clone(); - let download_dir = download_dir.clone(); + let container_dir: Arc = Arc::from(download_dir.join(adapter.name.0.as_ref())); let root_path = root_path.clone(); let adapter = adapter.clone(); let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); - let task = cx.spawn(|cx| async move { - login_shell_env_loaded.await; + let task = { + let container_dir = container_dir.clone(); + cx.spawn(|mut cx| async move { + login_shell_env_loaded.await; - let mut lock = this.lsp_binary_paths.lock(); - let entry = lock - .entry(adapter.name.clone()) - .or_insert_with(|| { - get_binary( - adapter.clone(), - language.clone(), - http_client, - download_dir, - lsp_binary_statuses, - ) - .map_err(Arc::new) - .boxed() - .shared() - }) - .clone(); - drop(lock); - let binary = entry.clone().map_err(|e| anyhow!(e)).await?; + let mut lock = this.lsp_binary_paths.lock(); + let entry = lock + .entry(adapter.name.clone()) + .or_insert_with(|| { + cx.spawn(|cx| { + get_binary( + adapter.clone(), + language.clone(), + delegate.clone(), + container_dir, + lsp_binary_statuses, + cx, + ) + .map_err(Arc::new) + }) + .shared() + }) + .clone(); + drop(lock); - let server = lsp::LanguageServer::new( - server_id, - &binary.path, - &binary.arguments, - &root_path, - adapter.code_action_kinds(), - cx, - )?; + let binary = match entry.clone().await.log_err() { + Some(binary) => binary, + None => return Ok(None), + }; - Ok(server) - }); + if let Some(task) = adapter.will_start_server(&delegate, &mut cx) { + if task.await.log_err().is_none() { + return Ok(None); + } + } - Some(PendingLanguageServer { server_id, task }) + Ok(Some(lsp::LanguageServer::new( + server_id, + binary, + &root_path, + adapter.code_action_kinds(), + cx, + )?)) + }) + }; + + Some(PendingLanguageServer { + server_id, + task, + container_dir: Some(container_dir), + }) } pub fn language_server_binary_statuses( @@ -909,6 +984,30 @@ impl LanguageRegistry { ) -> async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)> { self.lsp_binary_statuses_rx.clone() } + + pub fn delete_server_container( + &self, + adapter: Arc, + cx: &mut AppContext, + ) -> Task<()> { + log::info!("deleting server container"); + + let mut lock = self.lsp_binary_paths.lock(); + lock.remove(&adapter.name); + + let download_dir = self + .language_server_download_dir + .clone() + .expect("language server download directory has not been assigned before deleting server container"); + + cx.spawn(|_| async move { + let container_dir = download_dir.join(adapter.name.0.as_ref()); + smol::fs::remove_dir_all(container_dir) + .await + .context("server container removal") + .log_err(); + }) + } } impl LanguageRegistryState { @@ -958,32 +1057,39 @@ impl Default for LanguageRegistry { async fn get_binary( adapter: Arc, language: Arc, - http_client: Arc, - download_dir: Arc, + delegate: Arc, + container_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, + mut cx: AsyncAppContext, ) -> Result { - let container_dir = download_dir.join(adapter.name.0.as_ref()); if !container_dir.exists() { smol::fs::create_dir_all(&container_dir) .await .context("failed to create container directory")?; } + if let Some(task) = adapter.will_fetch_server(&delegate, &mut cx) { + task.await?; + } + let binary = fetch_latest_binary( adapter.clone(), language.clone(), - http_client, + delegate.as_ref(), &container_dir, statuses.clone(), ) .await; if let Err(error) = binary.as_ref() { - if let Some(cached) = adapter.cached_server_binary(container_dir).await { + if let Some(binary) = adapter + .cached_server_binary(container_dir.to_path_buf(), delegate.as_ref()) + .await + { statuses .broadcast((language.clone(), LanguageServerBinaryStatus::Cached)) .await?; - return Ok(cached); + return Ok(binary); } else { statuses .broadcast(( @@ -995,13 +1101,14 @@ async fn get_binary( .await?; } } + binary } async fn fetch_latest_binary( adapter: Arc, language: Arc, - http_client: Arc, + delegate: &dyn LspAdapterDelegate, container_dir: &Path, lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { @@ -1012,18 +1119,19 @@ async fn fetch_latest_binary( LanguageServerBinaryStatus::CheckingForUpdate, )) .await?; - let version_info = adapter - .fetch_latest_server_version(http_client.clone()) - .await?; + + let version_info = adapter.fetch_latest_server_version(delegate).await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloading)) .await?; + let binary = adapter - .fetch_server_binary(version_info, http_client, container_dir.to_path_buf()) + .fetch_server_binary(version_info, container_dir.to_path_buf(), delegate) .await?; lsp_binary_statuses_tx .broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded)) .await?; + Ok(binary) } @@ -1543,7 +1651,7 @@ impl LspAdapter for Arc { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { unreachable!(); } @@ -1551,13 +1659,21 @@ impl LspAdapter for Arc { async fn fetch_server_binary( &self, _: Box, - _: Arc, _: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { unreachable!(); } - async fn cached_server_binary(&self, _: PathBuf) -> Option { + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + unreachable!(); + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { unreachable!(); } diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 832bb59222..820217567a 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,6 +1,6 @@ use crate::{File, Language}; use anyhow::Result; -use collections::HashMap; +use collections::{HashMap, HashSet}; use globset::GlobMatcher; use gpui::AppContext; use schemars::{ @@ -52,6 +52,7 @@ pub struct LanguageSettings { pub show_copilot_suggestions: bool, pub show_whitespaces: ShowWhitespaceSetting, pub extend_comment_on_newline: bool, + pub inlay_hints: InlayHintSettings, } #[derive(Clone, Debug, Default)] @@ -98,6 +99,8 @@ pub struct LanguageSettingsContent { pub show_whitespaces: Option, #[serde(default)] pub extend_comment_on_newline: Option, + #[serde(default)] + pub inlay_hints: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -150,6 +153,38 @@ pub enum Formatter { }, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct InlayHintSettings { + #[serde(default)] + pub enabled: bool, + #[serde(default = "default_true")] + pub show_type_hints: bool, + #[serde(default = "default_true")] + pub show_parameter_hints: bool, + #[serde(default = "default_true")] + pub show_other_hints: bool, +} + +fn default_true() -> bool { + true +} + +impl InlayHintSettings { + pub fn enabled_inlay_hint_kinds(&self) -> HashSet> { + let mut kinds = HashSet::default(); + if self.show_type_hints { + kinds.insert(Some(InlayHintKind::Type)); + } + if self.show_parameter_hints { + kinds.insert(Some(InlayHintKind::Parameter)); + } + if self.show_other_hints { + kinds.insert(None); + } + kinds + } +} + impl AllLanguageSettings { pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings { if let Some(name) = language_name { @@ -184,6 +219,29 @@ impl AllLanguageSettings { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum InlayHintKind { + Type, + Parameter, +} + +impl InlayHintKind { + pub fn from_name(name: &str) -> Option { + match name { + "type" => Some(InlayHintKind::Type), + "parameter" => Some(InlayHintKind::Parameter), + _ => None, + } + } + + pub fn name(&self) -> &'static str { + match self { + InlayHintKind::Type => "type", + InlayHintKind::Parameter => "parameter", + } + } +} + impl settings::Setting for AllLanguageSettings { const KEY: Option<&'static str> = None; @@ -347,6 +405,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent &mut settings.extend_comment_on_newline, src.extend_comment_on_newline, ); + merge(&mut settings.inlay_hints, src.inlay_hints); fn merge(target: &mut T, value: Option) { if let Some(value) = value { *target = value; diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 7e5664c1bd..1570baf185 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -11,7 +11,7 @@ use std::{ cell::RefCell, cmp::{self, Ordering, Reverse}, collections::BinaryHeap, - iter, + fmt, iter, ops::{Deref, DerefMut, Range}, sync::Arc, }; @@ -288,7 +288,7 @@ impl SyntaxSnapshot { }; if target.cmp(&cursor.start(), text).is_gt() { let slice = cursor.slice(&target, Bias::Left, text); - layers.push_tree(slice, text); + layers.append(slice, text); } } // If this layer follows all of the edits, then preserve it and any @@ -303,7 +303,7 @@ impl SyntaxSnapshot { Bias::Left, text, ); - layers.push_tree(slice, text); + layers.append(slice, text); continue; }; @@ -369,7 +369,7 @@ impl SyntaxSnapshot { cursor.next(text); } - layers.push_tree(cursor.suffix(&text), &text); + layers.append(cursor.suffix(&text), &text); drop(cursor); self.layers = layers; } @@ -428,6 +428,8 @@ impl SyntaxSnapshot { invalidated_ranges: Vec>, registry: Option<&Arc>, ) { + log::trace!("reparse. invalidated ranges:{:?}", invalidated_ranges); + let max_depth = self.layers.summary().max_depth; let mut cursor = self.layers.cursor::(); cursor.next(&text); @@ -478,7 +480,7 @@ impl SyntaxSnapshot { if bounded_position.cmp(&cursor.start(), &text).is_gt() { let slice = cursor.slice(&bounded_position, Bias::Left, text); if !slice.is_empty() { - layers.push_tree(slice, &text); + layers.append(slice, &text); if changed_regions.prune(cursor.end(text), text) { done = false; } @@ -489,6 +491,15 @@ impl SyntaxSnapshot { let Some(layer) = cursor.item() else { break }; if changed_regions.intersects(&layer, text) { + if let SyntaxLayerContent::Parsed { language, .. } = &layer.content { + log::trace!( + "discard layer. language:{}, range:{:?}. changed_regions:{:?}", + language.name(), + LogAnchorRange(&layer.range, text), + LogChangedRegions(&changed_regions, text), + ); + } + changed_regions.insert( ChangedRegion { depth: layer.depth + 1, @@ -541,26 +552,24 @@ impl SyntaxSnapshot { .to_ts_point(); } - if included_ranges.is_empty() { - included_ranges.push(tree_sitter::Range { - start_byte: 0, - end_byte: 0, - start_point: Default::default(), - end_point: Default::default(), - }); - } - - if let Some(SyntaxLayerContent::Parsed { tree: old_tree, .. }) = - old_layer.map(|layer| &layer.content) + if let Some((SyntaxLayerContent::Parsed { tree: old_tree, .. }, layer_start)) = + old_layer.map(|layer| (&layer.content, layer.range.start)) { + log::trace!( + "existing layer. language:{}, start:{:?}, ranges:{:?}", + language.name(), + LogPoint(layer_start.to_point(&text)), + LogIncludedRanges(&old_tree.included_ranges()) + ); + if let ParseMode::Combined { mut parent_layer_changed_ranges, .. } = step.mode { for range in &mut parent_layer_changed_ranges { - range.start -= step_start_byte; - range.end -= step_start_byte; + range.start = range.start.saturating_sub(step_start_byte); + range.end = range.end.saturating_sub(step_start_byte); } included_ranges = splice_included_ranges( @@ -570,6 +579,22 @@ impl SyntaxSnapshot { ); } + if included_ranges.is_empty() { + included_ranges.push(tree_sitter::Range { + start_byte: 0, + end_byte: 0, + start_point: Default::default(), + end_point: Default::default(), + }); + } + + log::trace!( + "update layer. language:{}, start:{:?}, ranges:{:?}", + language.name(), + LogAnchorRange(&step.range, text), + LogIncludedRanges(&included_ranges), + ); + tree = parse_text( grammar, text.as_rope(), @@ -586,6 +611,22 @@ impl SyntaxSnapshot { }), ); } else { + if included_ranges.is_empty() { + included_ranges.push(tree_sitter::Range { + start_byte: 0, + end_byte: 0, + start_point: Default::default(), + end_point: Default::default(), + }); + } + + log::trace!( + "create layer. language:{}, range:{:?}, included_ranges:{:?}", + language.name(), + LogAnchorRange(&step.range, text), + LogIncludedRanges(&included_ranges), + ); + tree = parse_text( grammar, text.as_rope(), @@ -613,6 +654,7 @@ impl SyntaxSnapshot { get_injections( config, text, + step.range.clone(), tree.root_node_with_offset( step_start_byte, step_start_point.to_ts_point(), @@ -1117,6 +1159,7 @@ fn parse_text( fn get_injections( config: &InjectionConfig, text: &BufferSnapshot, + outer_range: Range, node: Node, language_registry: &Arc, depth: usize, @@ -1153,16 +1196,17 @@ fn get_injections( continue; } - // Avoid duplicate matches if two changed ranges intersect the same injection. let content_range = content_ranges.first().unwrap().start_byte..content_ranges.last().unwrap().end_byte; - if let Some((last_pattern_ix, last_range)) = &prev_match { - if mat.pattern_index == *last_pattern_ix && content_range == *last_range { + + // Avoid duplicate matches if two changed ranges intersect the same injection. + if let Some((prev_pattern_ix, prev_range)) = &prev_match { + if mat.pattern_index == *prev_pattern_ix && content_range == *prev_range { continue; } } - prev_match = Some((mat.pattern_index, content_range.clone())); + prev_match = Some((mat.pattern_index, content_range.clone())); let combined = config.patterns[mat.pattern_index].combined; let mut language_name = None; @@ -1218,11 +1262,10 @@ fn get_injections( for (language, mut included_ranges) in combined_injection_ranges.drain() { included_ranges.sort_unstable(); - let range = text.anchor_before(node.start_byte())..text.anchor_after(node.end_byte()); queue.push(ParseStep { depth, language: ParseStepLanguage::Loaded { language }, - range, + range: outer_range.clone(), included_ranges, mode: ParseMode::Combined { parent_layer_range: node.start_byte()..node.end_byte(), @@ -1234,72 +1277,77 @@ fn get_injections( pub(crate) fn splice_included_ranges( mut ranges: Vec, - changed_ranges: &[Range], + removed_ranges: &[Range], new_ranges: &[tree_sitter::Range], ) -> Vec { - let mut changed_ranges = changed_ranges.into_iter().peekable(); - let mut new_ranges = new_ranges.into_iter().peekable(); + let mut removed_ranges = removed_ranges.iter().cloned().peekable(); + let mut new_ranges = new_ranges.into_iter().cloned().peekable(); let mut ranges_ix = 0; loop { - let new_range = new_ranges.peek(); - let mut changed_range = changed_ranges.peek(); + let next_new_range = new_ranges.peek(); + let next_removed_range = removed_ranges.peek(); - // Remove ranges that have changed before inserting any new ranges - // into those ranges. - if let Some((changed, new)) = changed_range.zip(new_range) { - if new.end_byte < changed.start { - changed_range = None; + let (remove, insert) = match (next_removed_range, next_new_range) { + (None, None) => break, + (Some(_), None) => (removed_ranges.next().unwrap(), None), + (Some(next_removed_range), Some(next_new_range)) => { + if next_removed_range.end < next_new_range.start_byte { + (removed_ranges.next().unwrap(), None) + } else { + let mut start = next_new_range.start_byte; + let mut end = next_new_range.end_byte; + + while let Some(next_removed_range) = removed_ranges.peek() { + if next_removed_range.start > next_new_range.end_byte { + break; + } + let next_removed_range = removed_ranges.next().unwrap(); + start = cmp::min(start, next_removed_range.start); + end = cmp::max(end, next_removed_range.end); + } + + (start..end, Some(new_ranges.next().unwrap())) + } + } + (None, Some(next_new_range)) => ( + next_new_range.start_byte..next_new_range.end_byte, + Some(new_ranges.next().unwrap()), + ), + }; + + let mut start_ix = ranges_ix + + match ranges[ranges_ix..].binary_search_by_key(&remove.start, |r| r.end_byte) { + Ok(ix) => ix, + Err(ix) => ix, + }; + let mut end_ix = ranges_ix + + match ranges[ranges_ix..].binary_search_by_key(&remove.end, |r| r.start_byte) { + Ok(ix) => ix + 1, + Err(ix) => ix, + }; + + // If there are empty ranges, then there may be multiple ranges with the same + // start or end. Expand the splice to include any adjacent ranges that touch + // the changed range. + while start_ix > 0 { + if ranges[start_ix - 1].end_byte == remove.start { + start_ix -= 1; + } else { + break; + } + } + while let Some(range) = ranges.get(end_ix) { + if range.start_byte == remove.end { + end_ix += 1; + } else { + break; } } - if let Some(changed) = changed_range { - let mut start_ix = ranges_ix - + match ranges[ranges_ix..].binary_search_by_key(&changed.start, |r| r.end_byte) { - Ok(ix) | Err(ix) => ix, - }; - let mut end_ix = ranges_ix - + match ranges[ranges_ix..].binary_search_by_key(&changed.end, |r| r.start_byte) { - Ok(ix) => ix + 1, - Err(ix) => ix, - }; - - // If there are empty ranges, then there may be multiple ranges with the same - // start or end. Expand the splice to include any adjacent ranges that touch - // the changed range. - while start_ix > 0 { - if ranges[start_ix - 1].end_byte == changed.start { - start_ix -= 1; - } else { - break; - } - } - while let Some(range) = ranges.get(end_ix) { - if range.start_byte == changed.end { - end_ix += 1; - } else { - break; - } - } - - if end_ix > start_ix { - ranges.splice(start_ix..end_ix, []); - } - changed_ranges.next(); - ranges_ix = start_ix; - } else if let Some(new_range) = new_range { - let ix = ranges_ix - + match ranges[ranges_ix..] - .binary_search_by_key(&new_range.start_byte, |r| r.start_byte) - { - Ok(ix) | Err(ix) => ix, - }; - ranges.insert(ix, **new_range); - new_ranges.next(); - ranges_ix = ix + 1; - } else { - break; - } + ranges.splice(start_ix..end_ix, insert); + ranges_ix = start_ix; } + ranges } @@ -1628,3 +1676,46 @@ impl ToTreeSitterPoint for Point { Point::new(point.row as u32, point.column as u32) } } + +struct LogIncludedRanges<'a>(&'a [tree_sitter::Range]); +struct LogPoint(Point); +struct LogAnchorRange<'a>(&'a Range, &'a text::BufferSnapshot); +struct LogChangedRegions<'a>(&'a ChangeRegionSet, &'a text::BufferSnapshot); + +impl<'a> fmt::Debug for LogIncludedRanges<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list() + .entries(self.0.iter().map(|range| { + let start = range.start_point; + let end = range.end_point; + (start.row, start.column)..(end.row, end.column) + })) + .finish() + } +} + +impl<'a> fmt::Debug for LogAnchorRange<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let range = self.0.to_point(self.1); + (LogPoint(range.start)..LogPoint(range.end)).fmt(f) + } +} + +impl<'a> fmt::Debug for LogChangedRegions<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list() + .entries( + self.0 + .0 + .iter() + .map(|region| LogAnchorRange(®ion.range, self.1)), + ) + .finish() + } +} + +impl fmt::Debug for LogPoint { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (self.0.row, self.0.column).fmt(f) + } +} diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index 57b5cd4a8c..272501f2d0 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -48,6 +48,13 @@ fn test_splice_included_ranges() { let new_ranges = splice_included_ranges(ranges.clone(), &[30..50], &[ts_range(25..55)]); assert_eq!(new_ranges, &[ts_range(25..55), ts_range(80..90)]); + // does not create overlapping ranges + let new_ranges = splice_included_ranges(ranges.clone(), &[0..18], &[ts_range(20..32)]); + assert_eq!( + new_ranges, + &[ts_range(20..32), ts_range(50..60), ts_range(80..90)] + ); + fn ts_range(range: Range) -> tree_sitter::Range { tree_sitter::Range { start_byte: range.start, @@ -624,6 +631,26 @@ fn test_combined_injections_splitting_some_injections() { ); } +#[gpui::test] +fn test_combined_injections_editing_after_last_injection() { + test_edit_sequence( + "ERB", + &[ + r#" + <% foo %> +
+ <% bar %> + "#, + r#" + <% foo %> +
+ <% bar %>« + more text» + "#, + ], + ); +} + #[gpui::test] fn test_combined_injections_inside_injections() { let (_buffer, _syntax_map) = test_edit_sequence( @@ -974,13 +1001,16 @@ fn test_edit_sequence(language_name: &str, steps: &[&str]) -> (Buffer, SyntaxMap mutated_syntax_map.reparse(language.clone(), &buffer); for (i, marked_string) in steps.into_iter().enumerate() { - buffer.edit_via_marked_text(&marked_string.unindent()); + let marked_string = marked_string.unindent(); + log::info!("incremental parse {i}: {marked_string:?}"); + buffer.edit_via_marked_text(&marked_string); // Reparse the syntax map mutated_syntax_map.interpolate(&buffer); mutated_syntax_map.reparse(language.clone(), &buffer); // Create a second syntax map from scratch + log::info!("fresh parse {i}: {marked_string:?}"); let mut reference_syntax_map = SyntaxMap::new(); reference_syntax_map.set_language_registry(registry.clone()); reference_syntax_map.reparse(language.clone(), &buffer); @@ -1133,6 +1163,7 @@ fn range_for_text(buffer: &Buffer, text: &str) -> Range { start..start + text.len() } +#[track_caller] fn assert_layers_for_range( syntax_map: &SyntaxMap, buffer: &BufferSnapshot, diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 2c78b89f31..b97417580f 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -55,7 +55,7 @@ impl View for ActiveBufferLanguage { MouseEventHandler::::new(0, cx, |state, cx| { let theme = &theme::current(cx).workspace.status_bar; - let style = theme.active_language.style_for(state, false); + let style = theme.active_language.style_for(state); Label::new(active_language_text, style.text.clone()) .contained() .with_style(style.container) diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index 817901cd3a..6362b8247d 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -180,7 +180,7 @@ impl PickerDelegate for LanguageSelectorDelegate { ) -> AnyElement> { let theme = theme::current(cx); let mat = &self.matches[ix]; - let style = theme.picker.item.style_for(mouse_state, selected); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); let mut label = mat.string.clone(); if buffer_language_name.as_deref() == Some(mat.string.as_str()) { diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 04f47885c0..12d8c6b34d 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -681,7 +681,7 @@ impl LspLogToolbarItemView { ) }) .unwrap_or_else(|| "No server selected".into()); - let style = theme.toolbar_dropdown_menu.header.style_for(state, false); + let style = theme.toolbar_dropdown_menu.header.style_for(state); Label::new(label, style.text.clone()) .contained() .with_style(style.container) @@ -722,7 +722,8 @@ impl LspLogToolbarItemView { let style = theme .toolbar_dropdown_menu .item - .style_for(state, logs_selected); + .in_state(logs_selected) + .style_for(state); Label::new(SERVER_LOGS, style.text.clone()) .contained() .with_style(style.container) @@ -739,7 +740,8 @@ impl LspLogToolbarItemView { let style = theme .toolbar_dropdown_menu .item - .style_for(state, rpc_trace_selected); + .in_state(rpc_trace_selected) + .style_for(state); Flex::row() .with_child( Label::new(RPC_MESSAGES, style.text.clone()) diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 075df76653..3e6727bbf4 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -565,7 +565,7 @@ impl SyntaxTreeToolbarItemView { ) -> impl Element { enum ToggleMenu {} MouseEventHandler::::new(0, cx, move |state, _| { - let style = theme.toolbar_dropdown_menu.header.style_for(state, false); + let style = theme.toolbar_dropdown_menu.header.style_for(state); Flex::row() .with_child( Label::new(active_layer.language.name().to_string(), style.text.clone()) @@ -601,7 +601,8 @@ impl SyntaxTreeToolbarItemView { let style = theme .toolbar_dropdown_menu .item - .style_for(state, is_selected); + .in_state(is_selected) + .style_for(state); Flex::row() .with_child( Label::new(layer.language.name().to_string(), style.text.clone()) diff --git a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift index a0326b24a1..40d3641db2 100644 --- a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift +++ b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift @@ -6,19 +6,31 @@ import ScreenCaptureKit class LKRoomDelegate: RoomDelegate { var data: UnsafeRawPointer var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void + var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void + var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void + var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void + var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void var onDidSubscribeToRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void var onDidUnsubscribeFromRemoteVideoTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void - + init( data: UnsafeRawPointer, onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void, + onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, + onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, + onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, + onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void) { self.data = data self.onDidDisconnect = onDidDisconnect + self.onDidSubscribeToRemoteAudioTrack = onDidSubscribeToRemoteAudioTrack + self.onDidUnsubscribeFromRemoteAudioTrack = onDidUnsubscribeFromRemoteAudioTrack self.onDidSubscribeToRemoteVideoTrack = onDidSubscribeToRemoteVideoTrack self.onDidUnsubscribeFromRemoteVideoTrack = onDidUnsubscribeFromRemoteVideoTrack + self.onMuteChangedFromRemoteAudioTrack = onMuteChangedFromRemoteAudioTrack + self.onActiveSpeakersChanged = onActiveSpeakersChanged } func room(_ room: Room, didUpdate connectionState: ConnectionState, oldValue: ConnectionState) { @@ -30,12 +42,27 @@ class LKRoomDelegate: RoomDelegate { func room(_ room: Room, participant: RemoteParticipant, didSubscribe publication: RemoteTrackPublication, track: Track) { if track.kind == .video { self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque()) + } else if track.kind == .audio { + self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque()) } } + + func room(_ room: Room, participant: Participant, didUpdate publication: TrackPublication, muted: Bool) { + if publication.kind == .audio { + self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted) + } + } + + func room(_ room: Room, didUpdate speakers: [Participant]) { + guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return } + self.onActiveSpeakersChanged(self.data, speaker_ids) + } func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) { if track.kind == .video { self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString) + } else if track.kind == .audio { + self.onDidUnsubscribeFromRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString) } } } @@ -77,12 +104,20 @@ class LKVideoRenderer: NSObject, VideoRenderer { public func LKRoomDelegateCreate( data: UnsafeRawPointer, onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void, + onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, + onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, + onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, + onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void, onDidSubscribeToRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteVideoTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void ) -> UnsafeMutableRawPointer { let delegate = LKRoomDelegate( data: data, onDidDisconnect: onDidDisconnect, + onDidSubscribeToRemoteAudioTrack: onDidSubscribeToRemoteAudioTrack, + onDidUnsubscribeFromRemoteAudioTrack: onDidUnsubscribeFromRemoteAudioTrack, + onMuteChangedFromRemoteAudioTrack: onMuteChangedFromRemoteAudioTrack, + onActiveSpeakersChanged: onActiveSpeakerChanged, onDidSubscribeToRemoteVideoTrack: onDidSubscribeToRemoteVideoTrack, onDidUnsubscribeFromRemoteVideoTrack: onDidUnsubscribeFromRemoteVideoTrack ) @@ -123,6 +158,18 @@ public func LKRoomPublishVideoTrack(room: UnsafeRawPointer, track: UnsafeRawPoin } } +@_cdecl("LKRoomPublishAudioTrack") +public func LKRoomPublishAudioTrack(room: UnsafeRawPointer, track: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, UnsafeMutableRawPointer?, CFString?) -> Void, callback_data: UnsafeRawPointer) { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + room.localParticipant?.publishAudioTrack(track: track).then { publication in + callback(callback_data, Unmanaged.passRetained(publication).toOpaque(), nil) + }.catch { error in + callback(callback_data, nil, error.localizedDescription as CFString) + } +} + + @_cdecl("LKRoomUnpublishTrack") public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawPointer) { let room = Unmanaged.fromOpaque(room).takeUnretainedValue() @@ -130,6 +177,32 @@ public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawP let _ = room.localParticipant?.unpublish(publication: publication) } +@_cdecl("LKRoomAudioTracksForRemoteParticipant") +public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + + for (_, participant) in room.remoteParticipants { + if participant.identity == participantId as String { + return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray? + } + } + + return nil; +} + +@_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant") +public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { + let room = Unmanaged.fromOpaque(room).takeUnretainedValue() + + for (_, participant) in room.remoteParticipants { + if participant.identity == participantId as String { + return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray? + } + } + + return nil; +} + @_cdecl("LKRoomVideoTracksForRemoteParticipant") public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { let room = Unmanaged.fromOpaque(room).takeUnretainedValue() @@ -143,6 +216,17 @@ public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, partic return nil; } +@_cdecl("LKLocalAudioTrackCreateTrack") +public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer { + let track = LocalAudioTrack.createTrack(options: AudioCaptureOptions( + echoCancellation: true, + noiseSuppression: true + )) + + return Unmanaged.passRetained(track).toOpaque() +} + + @_cdecl("LKCreateScreenShareTrackForDisplay") public func LKCreateScreenShareTrackForDisplay(display: UnsafeMutableRawPointer) -> UnsafeMutableRawPointer { let display = Unmanaged.fromOpaque(display).takeUnretainedValue() @@ -169,6 +253,12 @@ public func LKRemoteVideoTrackGetSid(track: UnsafeRawPointer) -> CFString { return track.sid! as CFString } +@_cdecl("LKRemoteAudioTrackGetSid") +public func LKRemoteAudioTrackGetSid(track: UnsafeRawPointer) -> CFString { + let track = Unmanaged.fromOpaque(track).takeUnretainedValue() + return track.sid! as CFString +} + @_cdecl("LKDisplaySources") public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @convention(c) (UnsafeRawPointer, CFArray?, CFString?) -> Void) { MacOSScreenCapturer.sources(for: .display, includeCurrentApplication: false, preferredMethod: .legacy).then { displaySources in @@ -177,3 +267,43 @@ public func LKDisplaySources(data: UnsafeRawPointer, callback: @escaping @conven callback(data, nil, error.localizedDescription as CFString) } } + +@_cdecl("LKLocalTrackPublicationSetMute") +public func LKLocalTrackPublicationSetMute( + publication: UnsafeRawPointer, + muted: Bool, + on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, + callback_data: UnsafeRawPointer +) { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + if muted { + publication.mute().then { + on_complete(callback_data, nil) + }.catch { error in + on_complete(callback_data, error.localizedDescription as CFString) + } + } else { + publication.unmute().then { + on_complete(callback_data, nil) + }.catch { error in + on_complete(callback_data, error.localizedDescription as CFString) + } + } +} + +@_cdecl("LKRemoteTrackPublicationSetEnabled") +public func LKRemoteTrackPublicationSetEnabled( + publication: UnsafeRawPointer, + enabled: Bool, + on_complete: @escaping @convention(c) (UnsafeRawPointer, CFString?) -> Void, + callback_data: UnsafeRawPointer +) { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + publication.set(enabled: enabled).then { + on_complete(callback_data, nil) + }.catch { error in + on_complete(callback_data, error.localizedDescription as CFString) + } +} diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/live_kit_client/examples/test_app.rs index 96480e92bc..f5f6d0e46f 100644 --- a/crates/live_kit_client/examples/test_app.rs +++ b/crates/live_kit_client/examples/test_app.rs @@ -1,6 +1,10 @@ +use std::time::Duration; + use futures::StreamExt; use gpui::{actions, keymap_matcher::Binding, Menu, MenuItem}; -use live_kit_client::{LocalVideoTrack, RemoteVideoTrackUpdate, Room}; +use live_kit_client::{ + LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room, +}; use live_kit_server::token::{self, VideoGrant}; use log::LevelFilter; use simplelog::SimpleLogger; @@ -11,6 +15,12 @@ fn main() { SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); gpui::App::new(()).unwrap().run(|cx| { + #[cfg(any(test, feature = "test-support"))] + println!("USING TEST LIVEKIT"); + + #[cfg(not(any(test, feature = "test-support")))] + println!("USING REAL LIVEKIT"); + cx.platform().activate(true); cx.add_global_action(quit); @@ -49,16 +59,14 @@ fn main() { let room_b = Room::new(); room_b.connect(&live_kit_url, &user2_token).await.unwrap(); - let mut track_changes = room_b.remote_video_track_updates(); + let mut audio_track_updates = room_b.remote_audio_track_updates(); + let audio_track = LocalAudioTrack::create(); + let audio_track_publication = room_a.publish_audio_track(&audio_track).await.unwrap(); - let displays = room_a.display_sources().await.unwrap(); - let display = displays.into_iter().next().unwrap(); - - let track_a = LocalVideoTrack::screen_share_for_display(&display); - let track_a_publication = room_a.publish_video_track(&track_a).await.unwrap(); - - if let RemoteVideoTrackUpdate::Subscribed(track) = track_changes.next().await.unwrap() { - let remote_tracks = room_b.remote_video_tracks("test-participant-1"); + if let RemoteAudioTrackUpdate::Subscribed(track) = + audio_track_updates.next().await.unwrap() + { + let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); assert_eq!(remote_tracks.len(), 1); assert_eq!(remote_tracks[0].publisher_id(), "test-participant-1"); assert_eq!(track.publisher_id(), "test-participant-1"); @@ -66,18 +74,92 @@ fn main() { panic!("unexpected message"); } - let remote_track = room_b + audio_track_publication.set_mute(true).await.unwrap(); + + println!("waiting for mute changed!"); + if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } = + audio_track_updates.next().await.unwrap() + { + let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); + assert_eq!(remote_tracks[0].sid(), track_id); + assert_eq!(muted, true); + } else { + panic!("unexpected message"); + } + + audio_track_publication.set_mute(false).await.unwrap(); + + if let RemoteAudioTrackUpdate::MuteChanged { track_id, muted } = + audio_track_updates.next().await.unwrap() + { + let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); + assert_eq!(remote_tracks[0].sid(), track_id); + assert_eq!(muted, false); + } else { + panic!("unexpected message"); + } + + println!("Pausing for 5 seconds to test audio, make some noise!"); + let timer = cx.background().timer(Duration::from_secs(5)); + timer.await; + let remote_audio_track = room_b + .remote_audio_tracks("test-participant-1") + .pop() + .unwrap(); + room_a.unpublish_track(audio_track_publication); + + // Clear out any active speakers changed messages + let mut next = audio_track_updates.next().await.unwrap(); + while let RemoteAudioTrackUpdate::ActiveSpeakersChanged { speakers } = next { + println!("Speakers changed: {:?}", speakers); + next = audio_track_updates.next().await.unwrap(); + } + + if let RemoteAudioTrackUpdate::Unsubscribed { + publisher_id, + track_id, + } = next + { + assert_eq!(publisher_id, "test-participant-1"); + assert_eq!(remote_audio_track.sid(), track_id); + assert_eq!(room_b.remote_audio_tracks("test-participant-1").len(), 0); + } else { + panic!("unexpected message"); + } + + let mut video_track_updates = room_b.remote_video_track_updates(); + let displays = room_a.display_sources().await.unwrap(); + let display = displays.into_iter().next().unwrap(); + + let local_video_track = LocalVideoTrack::screen_share_for_display(&display); + let local_video_track_publication = room_a + .publish_video_track(&local_video_track) + .await + .unwrap(); + + if let RemoteVideoTrackUpdate::Subscribed(track) = + video_track_updates.next().await.unwrap() + { + let remote_video_tracks = room_b.remote_video_tracks("test-participant-1"); + assert_eq!(remote_video_tracks.len(), 1); + assert_eq!(remote_video_tracks[0].publisher_id(), "test-participant-1"); + assert_eq!(track.publisher_id(), "test-participant-1"); + } else { + panic!("unexpected message"); + } + + let remote_video_track = room_b .remote_video_tracks("test-participant-1") .pop() .unwrap(); - room_a.unpublish_track(track_a_publication); + room_a.unpublish_track(local_video_track_publication); if let RemoteVideoTrackUpdate::Unsubscribed { publisher_id, track_id, - } = track_changes.next().await.unwrap() + } = video_track_updates.next().await.unwrap() { assert_eq!(publisher_id, "test-participant-1"); - assert_eq!(remote_track.sid(), track_id); + assert_eq!(remote_video_track.sid(), track_id); assert_eq!(room_b.remote_video_tracks("test-participant-1").len(), 0); } else { panic!("unexpected message"); diff --git a/crates/live_kit_client/src/live_kit_client.rs b/crates/live_kit_client/src/live_kit_client.rs index 2ded570828..0467018c00 100644 --- a/crates/live_kit_client/src/live_kit_client.rs +++ b/crates/live_kit_client/src/live_kit_client.rs @@ -4,7 +4,7 @@ pub mod prod; pub use prod::*; #[cfg(any(test, feature = "test-support"))] -mod test; +pub mod test; #[cfg(any(test, feature = "test-support"))] pub use test::*; diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index f45667e3c3..6daa0601ca 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -21,6 +21,26 @@ extern "C" { fn LKRoomDelegateCreate( callback_data: *mut c_void, on_did_disconnect: extern "C" fn(callback_data: *mut c_void), + on_did_subscribe_to_remote_audio_track: extern "C" fn( + callback_data: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + remote_track: *const c_void, + ), + on_did_unsubscribe_from_remote_audio_track: extern "C" fn( + callback_data: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + ), + on_mute_changed_from_remote_audio_track: extern "C" fn( + callback_data: *mut c_void, + track_id: CFStringRef, + muted: bool, + ), + on_active_speakers_changed: extern "C" fn( + callback_data: *mut c_void, + participants: CFArrayRef, + ), on_did_subscribe_to_remote_video_track: extern "C" fn( callback_data: *mut c_void, publisher_id: CFStringRef, @@ -49,7 +69,23 @@ extern "C" { callback: extern "C" fn(*mut c_void, *mut c_void, CFStringRef), callback_data: *mut c_void, ); + fn LKRoomPublishAudioTrack( + room: *const c_void, + track: *const c_void, + callback: extern "C" fn(*mut c_void, *mut c_void, CFStringRef), + callback_data: *mut c_void, + ); fn LKRoomUnpublishTrack(room: *const c_void, publication: *const c_void); + fn LKRoomAudioTracksForRemoteParticipant( + room: *const c_void, + participant_id: CFStringRef, + ) -> CFArrayRef; + + fn LKRoomAudioTrackPublicationsForRemoteParticipant( + room: *const c_void, + participant_id: CFStringRef, + ) -> CFArrayRef; + fn LKRoomVideoTracksForRemoteParticipant( room: *const c_void, participant_id: CFStringRef, @@ -61,6 +97,7 @@ extern "C" { on_drop: extern "C" fn(callback_data: *mut c_void), ) -> *const c_void; + fn LKRemoteAudioTrackGetSid(track: *const c_void) -> CFStringRef; fn LKVideoTrackAddRenderer(track: *const c_void, renderer: *const c_void); fn LKRemoteVideoTrackGetSid(track: *const c_void) -> CFStringRef; @@ -73,6 +110,21 @@ extern "C" { ), ); fn LKCreateScreenShareTrackForDisplay(display: *const c_void) -> *const c_void; + fn LKLocalAudioTrackCreateTrack() -> *const c_void; + + fn LKLocalTrackPublicationSetMute( + publication: *const c_void, + muted: bool, + on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef), + callback_data: *mut c_void, + ); + + fn LKRemoteTrackPublicationSetEnabled( + publication: *const c_void, + enabled: bool, + on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef), + callback_data: *mut c_void, + ); } pub type Sid = String; @@ -89,6 +141,7 @@ pub struct Room { watch::Sender, watch::Receiver, )>, + remote_audio_track_subscribers: Mutex>>, remote_video_track_subscribers: Mutex>>, _delegate: RoomDelegate, } @@ -100,6 +153,7 @@ impl Room { Self { native_room: unsafe { LKRoomCreate(delegate.native_delegate) }, connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)), + remote_audio_track_subscribers: Default::default(), remote_video_track_subscribers: Default::default(), _delegate: delegate, } @@ -174,7 +228,7 @@ impl Room { let tx = unsafe { Box::from_raw(tx as *mut oneshot::Sender>) }; if error.is_null() { - let _ = tx.send(Ok(LocalTrackPublication(publication))); + let _ = tx.send(Ok(LocalTrackPublication::new(publication))); } else { let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; let _ = tx.send(Err(anyhow!(error))); @@ -191,6 +245,32 @@ impl Room { async { rx.await.unwrap().context("error publishing video track") } } + pub fn publish_audio_track( + self: &Arc, + track: &LocalAudioTrack, + ) -> impl Future> { + let (tx, rx) = oneshot::channel::>(); + extern "C" fn callback(tx: *mut c_void, publication: *mut c_void, error: CFStringRef) { + let tx = + unsafe { Box::from_raw(tx as *mut oneshot::Sender>) }; + if error.is_null() { + let _ = tx.send(Ok(LocalTrackPublication::new(publication))); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + let _ = tx.send(Err(anyhow!(error))); + } + } + unsafe { + LKRoomPublishAudioTrack( + self.native_room, + track.0, + callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + ); + } + async { rx.await.unwrap().context("error publishing audio track") } + } + pub fn unpublish_track(&self, publication: LocalTrackPublication) { unsafe { LKRoomUnpublishTrack(self.native_room, publication.0); @@ -226,12 +306,112 @@ impl Room { } } + pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec> { + unsafe { + let tracks = LKRoomAudioTracksForRemoteParticipant( + self.native_room, + CFString::new(participant_id).as_concrete_TypeRef(), + ); + + if tracks.is_null() { + Vec::new() + } else { + let tracks = CFArray::wrap_under_get_rule(tracks); + tracks + .into_iter() + .map(|native_track| { + let native_track = *native_track; + let id = + CFString::wrap_under_get_rule(LKRemoteAudioTrackGetSid(native_track)) + .to_string(); + Arc::new(RemoteAudioTrack::new( + native_track, + id, + participant_id.into(), + )) + }) + .collect() + } + } + } + + pub fn remote_audio_track_publications( + &self, + participant_id: &str, + ) -> Vec> { + unsafe { + let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant( + self.native_room, + CFString::new(participant_id).as_concrete_TypeRef(), + ); + + if tracks.is_null() { + Vec::new() + } else { + let tracks = CFArray::wrap_under_get_rule(tracks); + tracks + .into_iter() + .map(|native_track_publication| { + let native_track_publication = *native_track_publication; + Arc::new(RemoteTrackPublication::new(native_track_publication)) + }) + .collect() + } + } + } + + pub fn remote_audio_track_updates(&self) -> mpsc::UnboundedReceiver { + let (tx, rx) = mpsc::unbounded(); + self.remote_audio_track_subscribers.lock().push(tx); + rx + } + pub fn remote_video_track_updates(&self) -> mpsc::UnboundedReceiver { let (tx, rx) = mpsc::unbounded(); self.remote_video_track_subscribers.lock().push(tx); rx } + fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack) { + let track = Arc::new(track); + self.remote_audio_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone())) + .is_ok() + }); + } + + fn did_unsubscribe_from_remote_audio_track(&self, publisher_id: String, track_id: String) { + self.remote_audio_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::Unsubscribed { + publisher_id: publisher_id.clone(), + track_id: track_id.clone(), + }) + .is_ok() + }); + } + + fn mute_changed_from_remote_audio_track(&self, track_id: String, muted: bool) { + self.remote_audio_track_subscribers.lock().retain(|tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::MuteChanged { + track_id: track_id.clone(), + muted, + }) + .is_ok() + }); + } + + // A vec of publisher IDs + fn active_speakers_changed(&self, speakers: Vec) { + self.remote_audio_track_subscribers + .lock() + .retain(move |tx| { + tx.unbounded_send(RemoteAudioTrackUpdate::ActiveSpeakersChanged { + speakers: speakers.clone(), + }) + .is_ok() + }); + } + fn did_subscribe_to_remote_video_track(&self, track: RemoteVideoTrack) { let track = Arc::new(track); self.remote_video_track_subscribers.lock().retain(|tx| { @@ -294,6 +474,10 @@ impl RoomDelegate { LKRoomDelegateCreate( weak_room as *mut c_void, Self::on_did_disconnect, + Self::on_did_subscribe_to_remote_audio_track, + Self::on_did_unsubscribe_from_remote_audio_track, + Self::on_mute_change_from_remote_audio_track, + Self::on_active_speakers_changed, Self::on_did_subscribe_to_remote_video_track, Self::on_did_unsubscribe_from_remote_video_track, ) @@ -312,6 +496,72 @@ impl RoomDelegate { let _ = Weak::into_raw(room); } + extern "C" fn on_did_subscribe_to_remote_audio_track( + room: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + track: *const c_void, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + let track = RemoteAudioTrack::new(track, track_id, publisher_id); + if let Some(room) = room.upgrade() { + room.did_subscribe_to_remote_audio_track(track); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_did_unsubscribe_from_remote_audio_track( + room: *mut c_void, + publisher_id: CFStringRef, + track_id: CFStringRef, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + if let Some(room) = room.upgrade() { + room.did_unsubscribe_from_remote_audio_track(publisher_id, track_id); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_mute_change_from_remote_audio_track( + room: *mut c_void, + track_id: CFStringRef, + muted: bool, + ) { + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; + if let Some(room) = room.upgrade() { + room.mute_changed_from_remote_audio_track(track_id, muted); + } + let _ = Weak::into_raw(room); + } + + extern "C" fn on_active_speakers_changed(room: *mut c_void, participants: CFArrayRef) { + if participants.is_null() { + return; + } + + let room = unsafe { Weak::from_raw(room as *mut Room) }; + let speakers = unsafe { + CFArray::wrap_under_get_rule(participants) + .into_iter() + .map( + |speaker: core_foundation::base::ItemRef<'_, *const c_void>| { + CFString::wrap_under_get_rule(*speaker as CFStringRef).to_string() + }, + ) + .collect() + }; + + if let Some(room) = room.upgrade() { + room.active_speakers_changed(speakers); + } + let _ = Weak::into_raw(room); + } + extern "C" fn on_did_subscribe_to_remote_video_track( room: *mut c_void, publisher_id: CFStringRef, @@ -352,6 +602,20 @@ impl Drop for RoomDelegate { } } +pub struct LocalAudioTrack(*const c_void); + +impl LocalAudioTrack { + pub fn create() -> Self { + Self(unsafe { LKLocalAudioTrackCreateTrack() }) + } +} + +impl Drop for LocalAudioTrack { + fn drop(&mut self) { + unsafe { CFRelease(self.0) } + } +} + pub struct LocalVideoTrack(*const c_void); impl LocalVideoTrack { @@ -368,12 +632,124 @@ impl Drop for LocalVideoTrack { pub struct LocalTrackPublication(*const c_void); +impl LocalTrackPublication { + pub fn new(native_track_publication: *const c_void) -> Self { + unsafe { + CFRetain(native_track_publication); + } + Self(native_track_publication) + } + + pub fn set_mute(&self, muted: bool) -> impl Future> { + let (tx, rx) = futures::channel::oneshot::channel(); + + extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) { + let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender>) }; + if error.is_null() { + tx.send(Ok(())).ok(); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + tx.send(Err(anyhow!(error))).ok(); + } + } + + unsafe { + LKLocalTrackPublicationSetMute( + self.0, + muted, + complete_callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + ) + } + + async move { rx.await.unwrap() } + } +} + impl Drop for LocalTrackPublication { fn drop(&mut self) { unsafe { CFRelease(self.0) } } } +pub struct RemoteTrackPublication(*const c_void); + +impl RemoteTrackPublication { + pub fn new(native_track_publication: *const c_void) -> Self { + unsafe { + CFRetain(native_track_publication); + } + Self(native_track_publication) + } + + pub fn set_enabled(&self, enabled: bool) -> impl Future> { + let (tx, rx) = futures::channel::oneshot::channel(); + + extern "C" fn complete_callback(callback_data: *mut c_void, error: CFStringRef) { + let tx = unsafe { Box::from_raw(callback_data as *mut oneshot::Sender>) }; + if error.is_null() { + tx.send(Ok(())).ok(); + } else { + let error = unsafe { CFString::wrap_under_get_rule(error).to_string() }; + tx.send(Err(anyhow!(error))).ok(); + } + } + + unsafe { + LKRemoteTrackPublicationSetEnabled( + self.0, + enabled, + complete_callback, + Box::into_raw(Box::new(tx)) as *mut c_void, + ) + } + + async move { rx.await.unwrap() } + } +} + +impl Drop for RemoteTrackPublication { + fn drop(&mut self) { + unsafe { CFRelease(self.0) } + } +} + +#[derive(Debug)] +pub struct RemoteAudioTrack { + _native_track: *const c_void, + sid: Sid, + publisher_id: String, +} + +impl RemoteAudioTrack { + fn new(native_track: *const c_void, sid: Sid, publisher_id: String) -> Self { + unsafe { + CFRetain(native_track); + } + Self { + _native_track: native_track, + sid, + publisher_id, + } + } + + pub fn sid(&self) -> &str { + &self.sid + } + + pub fn publisher_id(&self) -> &str { + &self.publisher_id + } + + pub fn enable(&self) -> impl Future> { + async { Ok(()) } + } + + pub fn disable(&self) -> impl Future> { + async { Ok(()) } + } +} + #[derive(Debug)] pub struct RemoteVideoTrack { native_track: *const c_void, @@ -453,6 +829,13 @@ pub enum RemoteVideoTrackUpdate { Unsubscribed { publisher_id: Sid, track_id: Sid }, } +pub enum RemoteAudioTrackUpdate { + ActiveSpeakersChanged { speakers: Vec }, + MuteChanged { track_id: Sid, muted: bool }, + Subscribed(Arc), + Unsubscribed { publisher_id: Sid, track_id: Sid }, +} + pub struct MacOSDisplay(*const c_void); impl MacOSDisplay { diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 8d1e4fa16a..3fc046c5a2 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -67,7 +67,7 @@ impl TestServer { } } - async fn create_room(&self, room: String) -> Result<()> { + pub async fn create_room(&self, room: String) -> Result<()> { self.background.simulate_random_delay().await; let mut server_rooms = self.rooms.lock(); if server_rooms.contains_key(&room) { @@ -104,7 +104,7 @@ impl TestServer { room_name )) } else { - for track in &room.tracks { + for track in &room.video_tracks { client_room .0 .lock() @@ -182,7 +182,7 @@ impl TestServer { frames_rx: local_track.frames_rx.clone(), }); - room.tracks.push(track.clone()); + room.video_tracks.push(track.clone()); for (id, client_room) in &room.client_rooms { if *id != identity { @@ -199,6 +199,43 @@ impl TestServer { Ok(()) } + async fn publish_audio_track( + &self, + token: String, + _local_track: &LocalAudioTrack, + ) -> Result<()> { + self.background.simulate_random_delay().await; + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let identity = claims.sub.unwrap().to_string(); + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + + let track = Arc::new(RemoteAudioTrack { + sid: nanoid::nanoid!(17), + publisher_id: identity.clone(), + }); + + room.audio_tracks.push(track.clone()); + + for (id, client_room) in &room.client_rooms { + if *id != identity { + let _ = client_room + .0 + .lock() + .audio_track_updates + .0 + .try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone())) + .unwrap(); + } + } + + Ok(()) + } + fn video_tracks(&self, token: String) -> Result>> { let claims = live_kit_server::token::validate(&token, &self.secret_key)?; let room_name = claims.video.room.unwrap(); @@ -207,14 +244,26 @@ impl TestServer { let room = server_rooms .get_mut(&*room_name) .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; - Ok(room.tracks.clone()) + Ok(room.video_tracks.clone()) + } + + fn audio_tracks(&self, token: String) -> Result>> { + let claims = live_kit_server::token::validate(&token, &self.secret_key)?; + let room_name = claims.video.room.unwrap(); + + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&*room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + Ok(room.audio_tracks.clone()) } } #[derive(Default)] struct TestServerRoom { client_rooms: HashMap>, - tracks: Vec>, + video_tracks: Vec>, + audio_tracks: Vec>, } impl TestServerRoom {} @@ -266,6 +315,10 @@ struct RoomState { watch::Receiver, ), display_sources: Vec, + audio_track_updates: ( + async_broadcast::Sender, + async_broadcast::Receiver, + ), video_track_updates: ( async_broadcast::Sender, async_broadcast::Receiver, @@ -286,6 +339,7 @@ impl Room { connection: watch::channel_with(ConnectionState::Disconnected), display_sources: Default::default(), video_track_updates: async_broadcast::broadcast(128), + audio_track_updates: async_broadcast::broadcast(128), }))) } @@ -327,8 +381,51 @@ impl Room { Ok(LocalTrackPublication) } } + pub fn publish_audio_track( + self: &Arc, + track: &LocalAudioTrack, + ) -> impl Future> { + let this = self.clone(); + let track = track.clone(); + async move { + this.test_server() + .publish_audio_track(this.token(), &track) + .await?; + Ok(LocalTrackPublication) + } + } - pub fn unpublish_track(&self, _: LocalTrackPublication) {} + pub fn unpublish_track(&self, _publication: LocalTrackPublication) {} + + pub fn remote_audio_tracks(&self, publisher_id: &str) -> Vec> { + if !self.is_connected() { + return Vec::new(); + } + + self.test_server() + .audio_tracks(self.token()) + .unwrap() + .into_iter() + .filter(|track| track.publisher_id() == publisher_id) + .collect() + } + + pub fn remote_audio_track_publications( + &self, + publisher_id: &str, + ) -> Vec> { + if !self.is_connected() { + return Vec::new(); + } + + self.test_server() + .audio_tracks(self.token()) + .unwrap() + .into_iter() + .filter(|track| track.publisher_id() == publisher_id) + .map(|_track| Arc::new(RemoteTrackPublication {})) + .collect() + } pub fn remote_video_tracks(&self, publisher_id: &str) -> Vec> { if !self.is_connected() { @@ -343,6 +440,10 @@ impl Room { .collect() } + pub fn remote_audio_track_updates(&self) -> impl Stream { + self.0.lock().audio_track_updates.1.clone() + } + pub fn remote_video_track_updates(&self) -> impl Stream { self.0.lock().video_track_updates.1.clone() } @@ -391,6 +492,20 @@ impl Drop for Room { pub struct LocalTrackPublication; +impl LocalTrackPublication { + pub fn set_mute(&self, _mute: bool) -> impl Future> { + async { Ok(()) } + } +} + +pub struct RemoteTrackPublication; + +impl RemoteTrackPublication { + pub fn set_enabled(&self, _enabled: bool) -> impl Future> { + async { Ok(()) } + } +} + #[derive(Clone)] pub struct LocalVideoTrack { frames_rx: async_broadcast::Receiver, @@ -404,6 +519,15 @@ impl LocalVideoTrack { } } +#[derive(Clone)] +pub struct LocalAudioTrack; + +impl LocalAudioTrack { + pub fn create() -> Self { + Self + } +} + pub struct RemoteVideoTrack { sid: Sid, publisher_id: Sid, @@ -424,12 +548,44 @@ impl RemoteVideoTrack { } } +#[derive(Debug)] +pub struct RemoteAudioTrack { + sid: Sid, + publisher_id: Sid, +} + +impl RemoteAudioTrack { + pub fn sid(&self) -> &str { + &self.sid + } + + pub fn publisher_id(&self) -> &str { + &self.publisher_id + } + + pub fn enable(&self) -> impl Future> { + async { Ok(()) } + } + + pub fn disable(&self) -> impl Future> { + async { Ok(()) } + } +} + #[derive(Clone)] pub enum RemoteVideoTrackUpdate { Subscribed(Arc), Unsubscribed { publisher_id: Sid, track_id: Sid }, } +#[derive(Clone)] +pub enum RemoteAudioTrackUpdate { + ActiveSpeakersChanged { speakers: Vec }, + MuteChanged { track_id: Sid, muted: bool }, + Subscribed(Arc), + Unsubscribed { publisher_id: Sid, track_id: Sid }, +} + #[derive(Clone)] pub struct MacOSDisplay { frames: ( diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 96d4382075..a01f6e8a49 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -16,6 +16,7 @@ use smol::{ process::{self, Child}, }; use std::{ + ffi::OsString, fmt, future::Future, io::Write, @@ -33,9 +34,15 @@ const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; type NotificationHandler = Box, &str, AsyncAppContext)>; -type ResponseHandler = Box)>; +type ResponseHandler = Box)>; type IoHandler = Box; +#[derive(Debug, Clone, Deserialize)] +pub struct LanguageServerBinary { + pub path: PathBuf, + pub arguments: Vec, +} + pub struct LanguageServer { server_id: LanguageServerId, next_id: AtomicUsize, @@ -51,7 +58,7 @@ pub struct LanguageServer { io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, root_path: PathBuf, - _server: Option, + _server: Option>, } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -103,14 +110,14 @@ struct Notification<'a, T> { params: T, } -#[derive(Deserialize)] +#[derive(Debug, Clone, Deserialize)] struct AnyNotification<'a> { #[serde(default)] id: Option, #[serde(borrow)] method: &'a str, - #[serde(borrow)] - params: &'a RawValue, + #[serde(borrow, default)] + params: Option<&'a RawValue>, } #[derive(Debug, Serialize, Deserialize)] @@ -119,10 +126,9 @@ struct Error { } impl LanguageServer { - pub fn new>( + pub fn new( server_id: LanguageServerId, - binary_path: &Path, - arguments: &[T], + binary: LanguageServerBinary, root_path: &Path, code_action_kinds: Option>, cx: AsyncAppContext, @@ -133,9 +139,9 @@ impl LanguageServer { root_path.parent().unwrap_or_else(|| Path::new("/")) }; - let mut server = process::Command::new(binary_path) + let mut server = process::Command::new(&binary.path) .current_dir(working_dir) - .args(arguments) + .args(binary.arguments) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) @@ -157,16 +163,20 @@ impl LanguageServer { "unhandled notification {}:\n{}", notification.method, serde_json::to_string_pretty( - &Value::from_str(notification.params.get()).unwrap() + ¬ification + .params + .and_then(|params| Value::from_str(params.get()).ok()) + .unwrap_or(Value::Null) ) - .unwrap() + .unwrap(), ); }, ); - if let Some(name) = binary_path.file_name() { + if let Some(name) = binary.path.file_name() { server.name = name.to_string_lossy().to_string(); } + Ok(server) } @@ -228,7 +238,7 @@ impl LanguageServer { io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), - _server: server, + _server: server.map(|server| Mutex::new(server)), } } @@ -279,7 +289,11 @@ impl LanguageServer { if let Ok(msg) = serde_json::from_slice::(&buffer) { if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { - handler(msg.id, msg.params.get(), cx.clone()); + handler( + msg.id, + &msg.params.map(|params| params.get()).unwrap_or("null"), + cx.clone(), + ); } else { on_unhandled_notification(msg); } @@ -295,9 +309,9 @@ impl LanguageServer { if let Some(error) = error { handler(Err(error)); } else if let Some(result) = result { - handler(Ok(result.get())); + handler(Ok(result.get().into())); } else { - handler(Ok("null")); + handler(Ok("null".into())); } } } else { @@ -374,6 +388,9 @@ impl LanguageServer { resolve_support: None, ..WorkspaceSymbolClientCapabilities::default() }), + inlay_hint: Some(InlayHintWorkspaceClientCapabilities { + refresh_support: Some(true), + }), ..Default::default() }), text_document: Some(TextDocumentClientCapabilities { @@ -415,6 +432,10 @@ impl LanguageServer { content_format: Some(vec![MarkupKind::Markdown]), ..Default::default() }), + inlay_hint: Some(InlayHintClientCapabilities { + resolve_support: None, + dynamic_registration: Some(false), + }), ..Default::default() }), experimental: Some(json!({ @@ -450,11 +471,13 @@ impl LanguageServer { let response_handlers = self.response_handlers.clone(); let next_id = AtomicUsize::new(self.next_id.load(SeqCst)); let outbound_tx = self.outbound_tx.clone(); + let executor = self.executor.clone(); let mut output_done = self.output_done_rx.lock().take().unwrap(); let shutdown_request = Self::request_internal::( &next_id, &response_handlers, &outbound_tx, + &executor, (), ); let exit = Self::notify_internal::(&outbound_tx, ()); @@ -591,6 +614,7 @@ impl LanguageServer { }) .detach(); } + Err(error) => { log::error!( "error deserializing {} request: {:?}, message: {:?}", @@ -651,6 +675,7 @@ impl LanguageServer { &self.next_id, &self.response_handlers, &self.outbound_tx, + &self.executor, params, ) } @@ -659,6 +684,7 @@ impl LanguageServer { next_id: &AtomicUsize, response_handlers: &Mutex>>, outbound_tx: &channel::Sender, + executor: &Arc, params: T::Params, ) -> impl 'static + Future> where @@ -679,15 +705,20 @@ impl LanguageServer { .as_mut() .ok_or_else(|| anyhow!("server shut down")) .map(|handlers| { + let executor = executor.clone(); handlers.insert( id, Box::new(move |result| { - let response = match result { - Ok(response) => serde_json::from_str(response) - .context("failed to deserialize response"), - Err(error) => Err(anyhow!("{}", error.message)), - }; - let _ = tx.send(response); + executor + .spawn(async move { + let response = match result { + Ok(response) => serde_json::from_str(&response) + .context("failed to deserialize response"), + Err(error) => Err(anyhow!("{}", error.message)), + }; + _ = tx.send(response); + }) + .detach(); }), ); }); @@ -828,7 +859,13 @@ impl LanguageServer { cx, move |msg| { notifications_tx - .try_send((msg.method.to_string(), msg.params.get().to_string())) + .try_send(( + msg.method.to_string(), + msg.params + .map(|raw_value| raw_value.get()) + .unwrap_or("null") + .to_string(), + )) .ok(); }, )), diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index fce0fdfe50..53635f2725 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -20,3 +20,4 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true smol.workspace = true +log.workspace = true diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index e2a8d0d003..27a763e7f8 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -1,21 +1,24 @@ use anyhow::{anyhow, bail, Context, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; +use futures::lock::Mutex; use futures::{future::Shared, FutureExt}; use gpui::{executor::Background, Task}; -use parking_lot::Mutex; use serde::Deserialize; use smol::{fs, io::BufReader, process::Command}; +use std::process::Output; use std::{ env::consts, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, OnceLock}, }; -use util::http::HttpClient; +use util::{http::HttpClient, ResultExt}; const VERSION: &str = "v18.15.0"; -#[derive(Deserialize)] +static RUNTIME_INSTANCE: OnceLock> = OnceLock::new(); + +#[derive(Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct NpmInfo { #[serde(default)] @@ -23,7 +26,7 @@ pub struct NpmInfo { versions: Vec, } -#[derive(Deserialize, Default)] +#[derive(Debug, Deserialize, Default)] pub struct NpmInfoDistTags { latest: Option, } @@ -35,12 +38,16 @@ pub struct NodeRuntime { } impl NodeRuntime { - pub fn new(http: Arc, background: Arc) -> Arc { - Arc::new(NodeRuntime { - http, - background, - installation_path: Mutex::new(None), - }) + pub fn instance(http: Arc, background: Arc) -> Arc { + RUNTIME_INSTANCE + .get_or_init(|| { + Arc::new(NodeRuntime { + http, + background, + installation_path: Mutex::new(None), + }) + }) + .clone() } pub async fn binary_path(&self) -> Result { @@ -50,55 +57,74 @@ impl NodeRuntime { pub async fn run_npm_subcommand( &self, - directory: &Path, + directory: Option<&Path>, subcommand: &str, args: &[&str], - ) -> Result<()> { + ) -> Result { + let attempt = |installation_path: PathBuf| async move { + let node_binary = installation_path.join("bin/node"); + let npm_file = installation_path.join("bin/npm"); + + if smol::fs::metadata(&node_binary).await.is_err() { + return Err(anyhow!("missing node binary file")); + } + + if smol::fs::metadata(&npm_file).await.is_err() { + return Err(anyhow!("missing npm file")); + } + + let mut command = Command::new(node_binary); + command.arg(npm_file).arg(subcommand).args(args); + + if let Some(directory) = directory { + command.current_dir(directory); + } + + command.output().await.map_err(|e| anyhow!("{e}")) + }; + let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join("bin/node"); - let npm_file = installation_path.join("bin/npm"); - - let output = Command::new(node_binary) - .arg(npm_file) - .arg(subcommand) - .args(args) - .current_dir(directory) - .output() - .await?; - - if !output.status.success() { - return Err(anyhow!( - "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); + let mut output = attempt(installation_path).await; + if output.is_err() { + let installation_path = self.reinstall().await?; + output = attempt(installation_path).await; + if output.is_err() { + return Err(anyhow!( + "failed to launch npm subcommand {subcommand} subcommand" + )); + } } - Ok(()) + if let Ok(output) = &output { + if !output.status.success() { + return Err(anyhow!( + "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + )); + } + } + + output.map_err(|e| anyhow!("{e}")) } pub async fn npm_package_latest_version(&self, name: &str) -> Result { - let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join("bin/node"); - let npm_file = installation_path.join("bin/npm"); - - let output = Command::new(node_binary) - .arg(npm_file) - .args(["-fetch-retry-mintimeout", "2000"]) - .args(["-fetch-retry-maxtimeout", "5000"]) - .args(["-fetch-timeout", "5000"]) - .args(["info", name, "--json"]) - .output() - .await - .context("failed to run npm info")?; - - if !output.status.success() { - return Err(anyhow!( - "failed to execute npm info:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } + let output = self + .run_npm_subcommand( + None, + "info", + &[ + name, + "--json", + "-fetch-retry-mintimeout", + "2000", + "-fetch-retry-maxtimeout", + "5000", + "-fetch-timeout", + "5000", + ], + ) + .await?; let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; info.dist_tags @@ -112,41 +138,54 @@ impl NodeRuntime { directory: &Path, packages: impl IntoIterator, ) -> Result<()> { - let installation_path = self.install_if_needed().await?; - let node_binary = installation_path.join("bin/node"); - let npm_file = installation_path.join("bin/npm"); + let packages: Vec<_> = packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")) + .collect(); - let output = Command::new(node_binary) - .arg(npm_file) - .args(["-fetch-retry-mintimeout", "2000"]) - .args(["-fetch-retry-maxtimeout", "5000"]) - .args(["-fetch-timeout", "5000"]) - .arg("install") - .arg("--prefix") - .arg(directory) - .args( - packages - .into_iter() - .map(|(name, version)| format!("{name}@{version}")), - ) - .output() - .await - .context("failed to run npm install")?; + let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); + arguments.extend_from_slice(&[ + "-fetch-retry-mintimeout", + "2000", + "-fetch-retry-maxtimeout", + "5000", + "-fetch-timeout", + "5000", + ]); - if !output.status.success() { - return Err(anyhow!( - "failed to execute npm install:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - )); - } + self.run_npm_subcommand(Some(directory), "install", &arguments) + .await?; Ok(()) } + async fn reinstall(&self) -> Result { + log::info!("beginnning to reinstall Node runtime"); + let mut installation_path = self.installation_path.lock().await; + + if let Some(task) = installation_path.as_ref().cloned() { + if let Ok(installation_path) = task.await { + smol::fs::remove_dir_all(&installation_path) + .await + .context("node dir removal") + .log_err(); + } + } + + let http = self.http.clone(); + let task = self + .background + .spawn(async move { Self::install(http).await.map_err(Arc::new) }) + .shared(); + + *installation_path = Some(task.clone()); + task.await.map_err(|e| anyhow!("{}", e)) + } + async fn install_if_needed(&self) -> Result { let task = self .installation_path .lock() + .await .get_or_insert_with(|| { let http = self.http.clone(); self.background @@ -155,13 +194,11 @@ impl NodeRuntime { }) .clone(); - match task.await { - Ok(path) => Ok(path), - Err(error) => Err(anyhow!("{}", error)), - } + task.await.map_err(|e| anyhow!("{}", e)) } async fn install(http: Arc) -> Result { + log::info!("installing Node runtime"); let arch = match consts::ARCH { "x86_64" => "x64", "aarch64" => "arm64", diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 1e364f5fc8..f93fa10052 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -204,7 +204,7 @@ impl PickerDelegate for OutlineViewDelegate { cx: &AppContext, ) -> AnyElement> { let theme = theme::current(cx); - let style = theme.picker.item.style_for(mouse_state, selected); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); let string_match = &self.matches[ix]; let outline_item = &self.outline.items[string_match.candidate_id]; diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 69f16e4949..d09de5320c 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -25,6 +25,7 @@ pub struct Picker { theme: Arc theme::Picker>>>, confirmed: bool, pending_update_matches: Task>, + has_focus: bool, } pub trait PickerDelegate: Sized + 'static { @@ -45,6 +46,18 @@ pub trait PickerDelegate: Sized + 'static { fn center_selection_after_match_updates(&self) -> bool { false } + fn render_header( + &self, + _cx: &mut ViewContext>, + ) -> Option>> { + None + } + fn render_footer( + &self, + _cx: &mut ViewContext>, + ) -> Option>> { + None + } } impl Entity for Picker { @@ -77,6 +90,7 @@ impl View for Picker { .contained() .with_style(editor_style), ) + .with_children(self.delegate.render_header(cx)) .with_children(if match_count == 0 { if query.is_empty() { None @@ -118,6 +132,7 @@ impl View for Picker { .into_any(), ) }) + .with_children(self.delegate.render_footer(cx)) .contained() .with_style(container_style) .constrained() @@ -132,13 +147,22 @@ impl View for Picker { } fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; if cx.is_self_focused() { cx.focus(&self.query_editor); } } + + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } } impl Modal for Picker { + fn has_focus(&self) -> bool { + self.has_focus + } + fn dismiss_on_event(event: &Self::Event) -> bool { matches!(event, PickerEvent::Dismiss) } @@ -183,6 +207,7 @@ impl Picker { theme, confirmed: false, pending_update_matches: Task::ready(None), + has_focus: false, }; this.update_matches(String::new(), cx); this diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index d6578c87ba..bfe5f89f68 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -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"] } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8435de71e2..eec64beb5a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,14 +1,15 @@ use crate::{ - DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project, - ProjectTransaction, + DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, + MarkupContent, Project, ProjectTransaction, }; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use fs::LineEnding; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ - language_settings::language_settings, + language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, @@ -126,6 +127,10 @@ pub(crate) struct OnTypeFormatting { pub push_to_history: bool, } +pub(crate) struct InlayHints { + pub range: Range, +} + pub(crate) struct FormattingOptions { tab_size: u32, } @@ -1780,3 +1785,343 @@ impl LspCommand for OnTypeFormatting { message.buffer_id } } + +#[async_trait(?Send)] +impl LspCommand for InlayHints { + type Response = Vec; + type LspRequest = lsp::InlayHintRequest; + type ProtoRequest = proto::InlayHints; + + fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { + let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false }; + match inlay_hint_provider { + lsp::OneOf::Left(enabled) => *enabled, + lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { + lsp::InlayHintServerCapabilities::Options(_) => true, + lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false, + }, + } + } + + fn to_lsp( + &self, + path: &Path, + buffer: &Buffer, + _: &Arc, + _: &AppContext, + ) -> lsp::InlayHintParams { + lsp::InlayHintParams { + text_document: lsp::TextDocumentIdentifier { + uri: lsp::Url::from_file_path(path).unwrap(), + }, + range: range_to_lsp(self.range.to_point_utf16(buffer)), + work_done_progress_params: Default::default(), + } + } + + async fn response_from_lsp( + self, + message: Option>, + project: ModelHandle, + buffer: ModelHandle, + server_id: LanguageServerId, + mut cx: AsyncAppContext, + ) -> Result> { + 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 + .unwrap_or_default() + .into_iter() + .map(|lsp_hint| { + let kind = lsp_hint.kind.and_then(|kind| match kind { + lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + _ => None, + }); + 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) { + origin_buffer.anchor_before(position) + } else { + origin_buffer.anchor_after(position) + }, + padding_left, + padding_right: lsp_hint.padding_right.unwrap_or(false), + label: match lsp_hint.label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => { + InlayHintLabel::LabelParts( + lsp_parts + .into_iter() + .map(|label_part| InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map( + |tooltip| { + match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent( + markup_content, + ) => InlayHintLabelPartTooltip::MarkupContent( + MarkupContent { + kind: format!("{:?}", markup_content.kind), + value: markup_content.value, + }, + ), + } + }, + ), + location: label_part.location.map(|lsp_location| { + let target_start = origin_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = origin_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + Location { + buffer: buffer.clone(), + range: origin_buffer.anchor_after(target_start) + ..origin_buffer.anchor_before(target_end), + } + }), + }) + .collect(), + ) + } + }, + kind, + tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp::InlayHintTooltip::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: format!("{:?}", markup_content.kind), + value: markup_content.value, + }) + } + }), + } + }) + .collect()) + }) + } + + fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { + proto::InlayHints { + project_id, + buffer_id: buffer.remote_id(), + start: Some(language::proto::serialize_anchor(&self.range.start)), + end: Some(language::proto::serialize_anchor(&self.range.end)), + version: serialize_version(&buffer.version()), + } + } + + async fn from_proto( + message: proto::InlayHints, + _: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result { + let start = message + .start + .and_then(language::proto::deserialize_anchor) + .context("invalid start")?; + let end = message + .end + .and_then(language::proto::deserialize_anchor) + .context("invalid end")?; + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + Ok(Self { range: start..end }) + } + + fn response_to_proto( + response: Vec, + _: &mut Project, + _: PeerId, + buffer_version: &clock::Global, + cx: &mut AppContext, + ) -> proto::InlayHintsResponse { + proto::InlayHintsResponse { + hints: response + .into_iter() + .map(|response_hint| proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } + }), + }) + .collect(), + version: serialize_version(buffer_version), + } + } + + async fn response_from_proto( + self, + message: proto::InlayHintsResponse, + project: ModelHandle, + buffer: ModelHandle, + mut cx: AsyncAppContext, + ) -> Result> { + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(deserialize_version(&message.version)) + }) + .await?; + + let mut hints = Vec::new(); + for message_hint in message.hints { + let buffer_id = message_hint + .position + .as_ref() + .and_then(|location| location.buffer_id) + .context("missing buffer id")?; + let hint = InlayHint { + buffer_id, + position: message_hint + .position + .and_then(language::proto::deserialize_anchor) + .context("invalid position")?, + label: match message_hint + .label + .and_then(|label| label.label) + .context("missing label")? + { + proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), + proto::inlay_hint_label::Label::LabelParts(parts) => { + let mut label_parts = Vec::new(); + for part in parts.parts { + label_parts.push(InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.map(|tooltip| match tooltip.content { + Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s), + Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + None => InlayHintLabelPartTooltip::String(String::new()), + }), + location: match part.location { + Some(location) => { + let target_buffer = project + .update(&mut cx, |this, cx| { + this.wait_for_remote_buffer(location.buffer_id, cx) + }) + .await?; + Some(Location { + range: location + .start + .and_then(language::proto::deserialize_anchor) + .context("invalid start")? + ..location + .end + .and_then(language::proto::deserialize_anchor) + .context("invalid end")?, + buffer: target_buffer, + })}, + None => None, + }, + }); + } + + InlayHintLabel::LabelParts(label_parts) + } + }, + padding_left: message_hint.padding_left, + padding_right: message_hint.padding_right, + kind: message_hint + .kind + .as_deref() + .and_then(InlayHintKind::from_name), + tooltip: message_hint.tooltip.and_then(|tooltip| { + Some(match tooltip.content? { + proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), + proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }) + } + }) + }), + }; + + hints.push(hint); + } + + Ok(hints) + } + + fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 { + message.buffer_id + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5069c805b7..bbb2064da2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -29,23 +29,24 @@ use gpui::{ AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle, }; +use itertools::Itertools; use language::{ - language_settings::{language_settings, FormatOnSave, Formatter}, + language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind}, point_to_lsp, proto::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, }, - range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel, + range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _, - Language, LanguageRegistry, LanguageServerName, LocalFile, OffsetRangeExt, Operation, Patch, - PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, - Unclipped, + Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt, + Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, + ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, - DocumentHighlightKind, LanguageServer, LanguageServerId, + DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId, OneOf, }; use lsp_command::*; use postage::watch; @@ -64,7 +65,8 @@ use std::{ mem, num::NonZeroU32, ops::Range, - path::{Component, Path, PathBuf}, + path::{self, Component, Path, PathBuf}, + process::Stdio, rc::Rc, str, sync::{ @@ -74,9 +76,10 @@ use std::{ time::{Duration, Instant}, }; use terminals::Terminals; +use text::Anchor; use util::{ - debug_panic, defer, merge_json_value_into, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, - ResultExt, TryFutureExt as _, + debug_panic, defer, http::HttpClient, merge_json_value_into, + paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, }; pub use fs::*; @@ -223,6 +226,7 @@ enum OpenBuffer { Operations(Vec), } +#[derive(Clone)] enum WorktreeHandle { Strong(ModelHandle), Weak(WeakModelHandle), @@ -252,6 +256,7 @@ pub enum Event { LanguageServerAdded(LanguageServerId), LanguageServerRemoved(LanguageServerId), LanguageServerLog(LanguageServerId, String), + Notification(String), ActiveEntryChanged(Option), WorktreeAdded, WorktreeRemoved(WorktreeId), @@ -274,10 +279,12 @@ pub enum Event { new_peer_id: proto::PeerId, }, CollaboratorLeft(proto::PeerId), + RefreshInlays, } pub enum LanguageServerState { Starting(Task>>), + Running { language: Arc, adapter: Arc, @@ -315,12 +322,63 @@ pub struct DiagnosticSummary { pub warning_count: usize, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Location { pub buffer: ModelHandle, pub range: Range, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct InlayHint { + pub buffer_id: u64, + pub position: language::Anchor, + pub label: InlayHintLabel, + pub kind: Option, + pub padding_left: bool, + pub padding_right: bool, + pub tooltip: Option, +} + +impl InlayHint { + pub fn text(&self) -> String { + match &self.label { + InlayHintLabel::String(s) => s.to_owned(), + InlayHintLabel::LabelParts(parts) => parts.iter().map(|part| &part.value).join(""), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum InlayHintLabel { + String(String), + LabelParts(Vec), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct InlayHintLabelPart { + pub value: String, + pub tooltip: Option, + pub location: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum InlayHintTooltip { + String(String), + MarkupContent(MarkupContent), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum InlayHintLabelPartTooltip { + String(String), + MarkupContent(MarkupContent), +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct MarkupContent { + pub kind: String, + pub value: String, +} + #[derive(Debug, Clone)] pub struct LocationLink { pub origin: Option, @@ -435,6 +493,11 @@ pub enum FormatTrigger { Manual, } +struct ProjectLspAdapterDelegate { + project: ModelHandle, + http_client: Arc, +} + impl FormatTrigger { fn from_proto(value: i32) -> FormatTrigger { match value { @@ -472,9 +535,12 @@ impl Project { client.add_model_request_handler(Self::handle_rename_project_entry); client.add_model_request_handler(Self::handle_copy_project_entry); client.add_model_request_handler(Self::handle_delete_project_entry); + client.add_model_request_handler(Self::handle_expand_project_entry); client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); + client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_synchronize_buffers); client.add_model_request_handler(Self::handle_format_buffers); @@ -1066,6 +1132,40 @@ impl Project { } } + pub fn expand_entry( + &mut self, + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let worktree = self.worktree_for_id(worktree_id, cx)?; + if self.is_local() { + worktree.update(cx, |worktree, cx| { + worktree.as_local_mut().unwrap().expand_entry(entry_id, cx) + }) + } else { + let worktree = worktree.downgrade(); + let request = self.client.request(proto::ExpandProjectEntry { + project_id: self.remote_id().unwrap(), + entry_id: entry_id.to_proto(), + }); + Some(cx.spawn_weak(|_, mut cx| async move { + let response = request.await?; + if let Some(worktree) = worktree.upgrade(&cx) { + worktree + .update(&mut cx, |worktree, _| { + worktree + .as_remote_mut() + .unwrap() + .wait_for_snapshot(response.worktree_scan_id as usize) + }) + .await?; + } + Ok(()) + })) + } + } + pub fn shared(&mut self, project_id: u64, cx: &mut ModelContext) -> Result<()> { if self.client_state.is_some() { return Err(anyhow!("project was already shared")); @@ -2383,348 +2483,524 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - if !language_settings( - Some(&language), - worktree - .update(cx, |tree, cx| tree.root_file(cx)) - .map(|f| f as _) - .as_ref(), - cx, - ) - .enable_language_server - { + let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx)); + let settings = language_settings(Some(&language), root_file.map(|f| f as _).as_ref(), cx); + if !settings.enable_language_server { return; } let worktree_id = worktree.read(cx).id(); for adapter in language.lsp_adapters() { - let key = (worktree_id, adapter.name.clone()); - if self.language_server_ids.contains_key(&key) { - continue; - } - - let pending_server = match self.languages.start_language_server( - language.clone(), - adapter.clone(), + self.start_language_server( + worktree_id, worktree_path.clone(), - self.client.http_client(), - cx, - ) { - Some(pending_server) => pending_server, - None => continue, - }; - - let lsp = settings::get::(cx) - .lsp - .get(&adapter.name.0); - let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); - - let mut initialization_options = adapter.initialization_options.clone(); - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } - - let server_id = pending_server.server_id; - let state = self.setup_pending_language_server( - initialization_options, - pending_server, adapter.clone(), language.clone(), - key.clone(), cx, ); - self.language_servers.insert(server_id, state); - self.language_server_ids.insert(key.clone(), server_id); } } - fn setup_pending_language_server( + fn start_language_server( &mut self, + worktree_id: WorktreeId, + worktree_path: Arc, + adapter: Arc, + language: Arc, + cx: &mut ModelContext, + ) { + let key = (worktree_id, adapter.name.clone()); + if self.language_server_ids.contains_key(&key) { + return; + } + + let pending_server = match self.languages.create_pending_language_server( + language.clone(), + adapter.clone(), + worktree_path, + ProjectLspAdapterDelegate::new(self, cx), + cx, + ) { + Some(pending_server) => pending_server, + None => return, + }; + + let project_settings = settings::get::(cx); + let lsp = project_settings.lsp.get(&adapter.name.0); + let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); + + let mut initialization_options = adapter.initialization_options.clone(); + match (&mut initialization_options, override_options) { + (Some(initialization_options), Some(override_options)) => { + merge_json_value_into(override_options, initialization_options); + } + (None, override_options) => initialization_options = override_options, + _ => {} + } + + let server_id = pending_server.server_id; + let container_dir = pending_server.container_dir.clone(); + let state = LanguageServerState::Starting({ + let adapter = adapter.clone(); + let server_name = adapter.name.0.clone(); + let languages = self.languages.clone(); + let language = language.clone(); + let key = key.clone(); + + cx.spawn_weak(|this, mut cx| async move { + let result = Self::setup_and_insert_language_server( + this, + initialization_options, + pending_server, + adapter.clone(), + languages, + language.clone(), + server_id, + key, + &mut cx, + ) + .await; + + match result { + Ok(server) => server, + + Err(err) => { + log::error!("failed to start language server {:?}: {}", server_name, err); + + if let Some(this) = this.upgrade(&cx) { + if let Some(container_dir) = container_dir { + let installation_test_binary = adapter + .installation_test_binary(container_dir.to_path_buf()) + .await; + + this.update(&mut cx, |_, cx| { + Self::check_errored_server( + language, + adapter, + server_id, + installation_test_binary, + cx, + ) + }); + } + } + + None + } + } + }) + }); + + self.language_servers.insert(server_id, state); + self.language_server_ids.insert(key, server_id); + } + + fn reinstall_language_server( + &mut self, + language: Arc, + adapter: Arc, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Option> { + log::info!("beginning to reinstall server"); + + let existing_server = match self.language_servers.remove(&server_id) { + Some(LanguageServerState::Running { server, .. }) => Some(server), + _ => None, + }; + + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade(cx) { + let key = (worktree.read(cx).id(), adapter.name.clone()); + self.language_server_ids.remove(&key); + } + } + + Some(cx.spawn(move |this, mut cx| async move { + if let Some(task) = existing_server.and_then(|server| server.shutdown()) { + log::info!("shutting down existing server"); + task.await; + } + + // TODO: This is race-safe with regards to preventing new instances from + // starting while deleting, but existing instances in other projects are going + // to be very confused and messed up + this.update(&mut cx, |this, cx| { + this.languages.delete_server_container(adapter.clone(), cx) + }) + .await; + + this.update(&mut cx, |this, mut cx| { + let worktrees = this.worktrees.clone(); + for worktree in worktrees { + let worktree = match worktree.upgrade(cx) { + Some(worktree) => worktree.read(cx), + None => continue, + }; + let worktree_id = worktree.id(); + let root_path = worktree.abs_path(); + + this.start_language_server( + worktree_id, + root_path, + adapter.clone(), + language.clone(), + &mut cx, + ); + } + }) + })) + } + + async fn setup_and_insert_language_server( + this: WeakModelHandle, initialization_options: Option, pending_server: PendingLanguageServer, adapter: Arc, + languages: Arc, language: Arc, + server_id: LanguageServerId, key: (WorktreeId, LanguageServerName), - cx: &mut ModelContext, - ) -> LanguageServerState { - let server_id = pending_server.server_id; - let languages = self.languages.clone(); + cx: &mut AsyncAppContext, + ) -> Result>> { + let setup = Self::setup_pending_language_server( + this, + initialization_options, + pending_server, + adapter.clone(), + languages, + server_id, + cx, + ); - LanguageServerState::Starting(cx.spawn_weak(|this, mut cx| async move { - let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; - let language_server = pending_server.task.await.log_err()?; + let language_server = match setup.await? { + Some(language_server) => language_server, + None => return Ok(None), + }; - language_server - .on_notification::({ - move |params, mut cx| { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |_, cx| { - cx.emit(Event::LanguageServerLog(server_id, params.message)) - }); - } - } - }) - .detach(); + let this = match this.upgrade(cx) { + Some(this) => this, + None => return Err(anyhow!("failed to upgrade project handle")), + }; - language_server - .on_notification::({ - let adapter = adapter.clone(); - move |mut params, cx| { - let adapter = adapter.clone(); - cx.spawn(|mut cx| async move { - adapter.process_diagnostics(&mut params).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.update_diagnostics( - server_id, - params, - &adapter.disk_based_diagnostic_sources, - cx, - ) - .log_err(); - }); - } - }) - .detach(); - } - }) - .detach(); + this.update(cx, |this, cx| { + this.insert_newly_running_language_server( + language, + adapter, + language_server.clone(), + server_id, + key, + cx, + ) + })?; - language_server - .on_request::({ - let languages = languages.clone(); - move |params, mut cx| { - let languages = languages.clone(); - async move { - let workspace_config = - cx.update(|cx| languages.workspace_configuration(cx)).await; - Ok(params - .items - .into_iter() - .map(|item| { - if let Some(section) = &item.section { - workspace_config - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null) - } else { - workspace_config.clone() - } - }) - .collect()) - } - } - }) - .detach(); + Ok(Some(language_server)) + } - // Even though we don't have handling for these requests, respond to them to - // avoid stalling any language server like `gopls` which waits for a response - // to these requests when initializing. - language_server - .on_request::( - move |params, mut cx| async move { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, _| { - if let Some(status) = - this.language_server_statuses.get_mut(&server_id) - { - if let lsp::NumberOrString::String(token) = params.token { - status.progress_tokens.insert(token); - } - } - }); - } - Ok(()) - }, - ) - .detach(); - language_server - .on_request::( - move |params, mut cx| async move { - let this = this - .upgrade(&cx) - .ok_or_else(|| anyhow!("project dropped"))?; - for reg in params.registrations { - if reg.method == "workspace/didChangeWatchedFiles" { - if let Some(options) = reg.register_options { - let options = serde_json::from_value(options)?; - this.update(&mut cx, |this, cx| { - this.on_lsp_did_change_watched_files( - server_id, options, cx, - ); - }); - } - } - } - Ok(()) - }, - ) - .detach(); + async fn setup_pending_language_server( + this: WeakModelHandle, + initialization_options: Option, + pending_server: PendingLanguageServer, + adapter: Arc, + languages: Arc, + server_id: LanguageServerId, + cx: &mut AsyncAppContext, + ) -> Result>> { + let workspace_config = cx.update(|cx| languages.workspace_configuration(cx)).await; + let language_server = match pending_server.task.await? { + Some(server) => server.initialize(initialization_options).await?, + None => { + return Ok(None); + } + }; - language_server - .on_request::({ - let adapter = adapter.clone(); - move |params, cx| { - Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx) - } - }) - .detach(); - - let disk_based_diagnostics_progress_token = - adapter.disk_based_diagnostics_progress_token.clone(); - - language_server - .on_notification::({ - move |params, mut cx| { - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.on_lsp_progress( - params, - server_id, - disk_based_diagnostics_progress_token.clone(), - cx, - ); - }); - } - } - }) - .detach(); - - let language_server = language_server - .initialize(initialization_options) - .await - .log_err()?; - language_server - .notify::( - lsp::DidChangeConfigurationParams { - settings: workspace_config, - }, - ) - .ok(); - - let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { - // If the language server for this key doesn't match the server id, don't store the - // server. Which will cause it to be dropped, killing the process - if this - .language_server_ids - .get(&key) - .map(|id| id != &server_id) - .unwrap_or(false) - { - return None; - } - - // Update language_servers collection with Running variant of LanguageServerState - // indicating that the server is up and running and ready - this.language_servers.insert( - server_id, - LanguageServerState::Running { - adapter: adapter.clone(), - language: language.clone(), - watched_paths: Default::default(), - server: language_server.clone(), - simulate_disk_based_diagnostics_completion: None, - }, - ); - this.language_server_statuses.insert( - server_id, - LanguageServerStatus { - name: language_server.name().to_string(), - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - }, - ); - - cx.emit(Event::LanguageServerAdded(server_id)); - - if let Some(project_id) = this.remote_id() { - this.client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: server_id.0 as u64, - name: language_server.name().to_string(), - }), - }) - .log_err(); - } - - // Tell the language server about every open buffer in the worktree that matches the language. - for buffer in this.opened_buffers.values() { - if let Some(buffer_handle) = buffer.upgrade(cx) { - let buffer = buffer_handle.read(cx); - let file = match File::from_dyn(buffer.file()) { - Some(file) => file, - None => continue, - }; - let language = match buffer.language() { - Some(language) => language, - None => continue, - }; - - if file.worktree.read(cx).id() != key.0 - || !language.lsp_adapters().iter().any(|a| a.name == key.1) - { - continue; - } - - let file = file.as_local()?; - let versions = this - .buffer_snapshots - .entry(buffer.remote_id()) - .or_default() - .entry(server_id) - .or_insert_with(|| { - vec![LspBufferSnapshot { - version: 0, - snapshot: buffer.text_snapshot(), - }] - }); - - let snapshot = versions.last().unwrap(); - let version = snapshot.version; - let initial_snapshot = &snapshot.snapshot; - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - language_server - .notify::( - lsp::DidOpenTextDocumentParams { - text_document: lsp::TextDocumentItem::new( - uri, - adapter - .language_ids - .get(language.name().as_ref()) - .cloned() - .unwrap_or_default(), - version, - initial_snapshot.text(), - ), - }, - ) - .log_err()?; - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - language_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| provider.trigger_characters.clone()) - .unwrap_or_default(), - cx, - ) + language_server + .on_notification::({ + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |_, cx| { + cx.emit(Event::LanguageServerLog(server_id, params.message)) }); } } - - cx.notify(); - Some(language_server) }) - })) + .detach(); + + language_server + .on_notification::({ + let adapter = adapter.clone(); + move |mut params, cx| { + let this = this; + let adapter = adapter.clone(); + cx.spawn(|mut cx| async move { + adapter.process_diagnostics(&mut params).await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.update_diagnostics( + server_id, + params, + &adapter.disk_based_diagnostic_sources, + cx, + ) + .log_err(); + }); + } + }) + .detach(); + } + }) + .detach(); + + language_server + .on_request::({ + let languages = languages.clone(); + move |params, mut cx| { + let languages = languages.clone(); + async move { + let workspace_config = + cx.update(|cx| languages.workspace_configuration(cx)).await; + Ok(params + .items + .into_iter() + .map(|item| { + if let Some(section) = &item.section { + workspace_config + .get(section) + .cloned() + .unwrap_or(serde_json::Value::Null) + } else { + workspace_config.clone() + } + }) + .collect()) + } + } + }) + .detach(); + + // Even though we don't have handling for these requests, respond to them to + // avoid stalling any language server like `gopls` which waits for a response + // to these requests when initializing. + language_server + .on_request::( + move |params, mut cx| async move { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, _| { + if let Some(status) = this.language_server_statuses.get_mut(&server_id) + { + if let lsp::NumberOrString::String(token) = params.token { + status.progress_tokens.insert(token); + } + } + }); + } + Ok(()) + }, + ) + .detach(); + language_server + .on_request::({ + move |params, mut cx| async move { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + for reg in params.registrations { + if reg.method == "workspace/didChangeWatchedFiles" { + if let Some(options) = reg.register_options { + let options = serde_json::from_value(options)?; + this.update(&mut cx, |this, cx| { + this.on_lsp_did_change_watched_files(server_id, options, cx); + }); + } + } + } + Ok(()) + } + }) + .detach(); + + language_server + .on_request::({ + let adapter = adapter.clone(); + move |params, cx| { + Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx) + } + }) + .detach(); + + language_server + .on_request::({ + move |(), mut cx| async move { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project dropped"))?; + this.update(&mut cx, |project, cx| { + cx.emit(Event::RefreshInlays); + project.remote_id().map(|project_id| { + project.client.send(proto::RefreshInlayHints { project_id }) + }) + }) + .transpose()?; + Ok(()) + } + }) + .detach(); + + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token.clone(); + + language_server + .on_notification::(move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_progress( + params, + server_id, + disk_based_diagnostics_progress_token.clone(), + cx, + ); + }); + } + }) + .detach(); + + language_server + .notify::( + lsp::DidChangeConfigurationParams { + settings: workspace_config, + }, + ) + .ok(); + + Ok(Some(language_server)) + } + + fn insert_newly_running_language_server( + &mut self, + language: Arc, + adapter: Arc, + language_server: Arc, + server_id: LanguageServerId, + key: (WorktreeId, LanguageServerName), + cx: &mut ModelContext, + ) -> Result<()> { + // If the language server for this key doesn't match the server id, don't store the + // server. Which will cause it to be dropped, killing the process + if self + .language_server_ids + .get(&key) + .map(|id| id != &server_id) + .unwrap_or(false) + { + return Ok(()); + } + + // Update language_servers collection with Running variant of LanguageServerState + // indicating that the server is up and running and ready + self.language_servers.insert( + server_id, + LanguageServerState::Running { + adapter: adapter.clone(), + language: language.clone(), + watched_paths: Default::default(), + server: language_server.clone(), + simulate_disk_based_diagnostics_completion: None, + }, + ); + + self.language_server_statuses.insert( + server_id, + LanguageServerStatus { + name: language_server.name().to_string(), + pending_work: Default::default(), + has_pending_diagnostic_updates: false, + progress_tokens: Default::default(), + }, + ); + + cx.emit(Event::LanguageServerAdded(server_id)); + + if let Some(project_id) = self.remote_id() { + self.client.send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id.0 as u64, + name: language_server.name().to_string(), + }), + })?; + } + + // Tell the language server about every open buffer in the worktree that matches the language. + for buffer in self.opened_buffers.values() { + if let Some(buffer_handle) = buffer.upgrade(cx) { + let buffer = buffer_handle.read(cx); + let file = match File::from_dyn(buffer.file()) { + Some(file) => file, + None => continue, + }; + let language = match buffer.language() { + Some(language) => language, + None => continue, + }; + + if file.worktree.read(cx).id() != key.0 + || !language.lsp_adapters().iter().any(|a| a.name == key.1) + { + continue; + } + + let file = match file.as_local() { + Some(file) => file, + None => continue, + }; + + let versions = self + .buffer_snapshots + .entry(buffer.remote_id()) + .or_default() + .entry(server_id) + .or_insert_with(|| { + vec![LspBufferSnapshot { + version: 0, + snapshot: buffer.text_snapshot(), + }] + }); + + let snapshot = versions.last().unwrap(); + let version = snapshot.version; + let initial_snapshot = &snapshot.snapshot; + let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + language_server.notify::( + lsp::DidOpenTextDocumentParams { + text_document: lsp::TextDocumentItem::new( + uri, + adapter + .language_ids + .get(language.name().as_ref()) + .cloned() + .unwrap_or_default(), + version, + initial_snapshot.text(), + ), + }, + )?; + + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( + language_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| provider.trigger_characters.clone()) + .unwrap_or_default(), + cx, + ) + }); + } + } + + cx.notify(); + Ok(()) } // Returns a list of all of the worktrees which no longer have a language server and the root path @@ -2773,9 +3049,7 @@ impl Project { let mut root_path = None; let server = match server_state { - Some(LanguageServerState::Starting(started_language_server)) => { - started_language_server.await - } + Some(LanguageServerState::Starting(task)) => task.await, Some(LanguageServerState::Running { server, .. }) => Some(server), None => None, }; @@ -2886,6 +3160,72 @@ impl Project { .detach(); } + fn check_errored_server( + language: Arc, + adapter: Arc, + server_id: LanguageServerId, + installation_test_binary: Option, + cx: &mut ModelContext, + ) { + if !adapter.can_be_reinstalled() { + log::info!( + "Validation check requested for {:?} but it cannot be reinstalled", + adapter.name.0 + ); + return; + } + + cx.spawn(|this, mut cx| async move { + log::info!("About to spawn test binary"); + + // A lack of test binary counts as a failure + let process = installation_test_binary.and_then(|binary| { + smol::process::Command::new(&binary.path) + .current_dir(&binary.path) + .args(binary.arguments) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .kill_on_drop(true) + .spawn() + .ok() + }); + + const PROCESS_TIMEOUT: Duration = Duration::from_secs(5); + let mut timeout = cx.background().timer(PROCESS_TIMEOUT).fuse(); + + let mut errored = false; + if let Some(mut process) = process { + futures::select! { + status = process.status().fuse() => match status { + Ok(status) => errored = !status.success(), + Err(_) => errored = true, + }, + + _ = timeout => { + log::info!("test binary time-ed out, this counts as a success"); + _ = process.kill(); + } + } + } else { + log::warn!("test binary failed to launch"); + errored = true; + } + + if errored { + log::warn!("test binary check failed"); + let task = this.update(&mut cx, move |this, mut cx| { + this.reinstall_language_server(language, adapter, server_id, &mut cx) + }); + + if let Some(task) = task { + task.await; + } + } + }) + .detach(); + } + fn on_lsp_progress( &mut self, progress: lsp::ProgressParams, @@ -3075,23 +3415,44 @@ impl Project { for watcher in params.watchers { for worktree in &self.worktrees { if let Some(worktree) = worktree.upgrade(cx) { - let worktree = worktree.read(cx); - if let Some(abs_path) = worktree.abs_path().to_str() { - if let Some(suffix) = match &watcher.glob_pattern { - lsp::GlobPattern::String(s) => s, - lsp::GlobPattern::Relative(rp) => &rp.pattern, - } - .strip_prefix(abs_path) - .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)) - { - if let Some(glob) = Glob::new(suffix).log_err() { - builders - .entry(worktree.id()) - .or_insert_with(|| GlobSetBuilder::new()) - .add(glob); + let glob_is_inside_worktree = worktree.update(cx, |tree, _| { + if let Some(abs_path) = tree.abs_path().to_str() { + let relative_glob_pattern = match &watcher.glob_pattern { + lsp::GlobPattern::String(s) => s + .strip_prefix(abs_path) + .and_then(|s| s.strip_prefix(std::path::MAIN_SEPARATOR)), + lsp::GlobPattern::Relative(rp) => { + let base_uri = match &rp.base_uri { + lsp::OneOf::Left(workspace_folder) => { + &workspace_folder.uri + } + lsp::OneOf::Right(base_uri) => base_uri, + }; + base_uri.to_file_path().ok().and_then(|file_path| { + (file_path.to_str() == Some(abs_path)) + .then_some(rp.pattern.as_str()) + }) + } + }; + if let Some(relative_glob_pattern) = relative_glob_pattern { + let literal_prefix = + glob_literal_prefix(&relative_glob_pattern); + tree.as_local_mut() + .unwrap() + .add_path_prefix_to_scan(Path::new(literal_prefix).into()); + if let Some(glob) = Glob::new(relative_glob_pattern).log_err() { + builders + .entry(tree.id()) + .or_insert_with(|| GlobSetBuilder::new()) + .add(glob); + } + return true; } - break; } + false + }); + if glob_is_inside_worktree { + break; } } } @@ -3657,14 +4018,15 @@ impl Project { tab_size: NonZeroU32, cx: &mut AsyncAppContext, ) -> Result, String)>> { - let text_document = - lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(abs_path).unwrap()); + let uri = lsp::Url::from_file_path(abs_path) + .map_err(|_| anyhow!("failed to convert abs path to uri"))?; + let text_document = lsp::TextDocumentIdentifier::new(uri); let capabilities = &language_server.capabilities(); - let lsp_edits = if capabilities - .document_formatting_provider - .as_ref() - .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) - { + + let formatting_provider = capabilities.document_formatting_provider.as_ref(); + let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); + + let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { language_server .request::(lsp::DocumentFormattingParams { text_document, @@ -3672,14 +4034,10 @@ impl Project { work_done_progress_params: Default::default(), }) .await? - } else if capabilities - .document_range_formatting_provider - .as_ref() - .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) - { + } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { let buffer_start = lsp::Position::new(0, 0); - let buffer_end = - buffer.read_with(cx, |buffer, _| point_to_lsp(buffer.max_point_utf16())); + let buffer_end = buffer.read_with(cx, |b, _| point_to_lsp(b.max_point_utf16())); + language_server .request::(lsp::DocumentRangeFormattingParams { text_document, @@ -3698,7 +4056,7 @@ impl Project { }) .await } else { - Ok(Default::default()) + Ok(Vec::new()) } } @@ -3806,70 +4164,72 @@ impl Project { let mut requests = Vec::new(); for ((worktree_id, _), server_id) in self.language_server_ids.iter() { let worktree_id = *worktree_id; - if let Some(worktree) = self - .worktree_for_id(worktree_id, cx) - .and_then(|worktree| worktree.read(cx).as_local()) - { - if let Some(LanguageServerState::Running { + let worktree_handle = self.worktree_for_id(worktree_id, cx); + let worktree = match worktree_handle.and_then(|tree| tree.read(cx).as_local()) { + Some(worktree) => worktree, + None => continue, + }; + let worktree_abs_path = worktree.abs_path().clone(); + + let (adapter, language, server) = match self.language_servers.get(server_id) { + Some(LanguageServerState::Running { adapter, language, server, .. - }) = self.language_servers.get(server_id) - { - let adapter = adapter.clone(); - let language = language.clone(); - let worktree_abs_path = worktree.abs_path().clone(); - requests.push( - server - .request::( - lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }, - ) - .log_err() - .map(move |response| { - let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { - lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { - flat_responses.into_iter().map(|lsp_symbol| { - (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) - }).collect::>() - } - lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { - nested_responses.into_iter().filter_map(|lsp_symbol| { - let location = match lsp_symbol.location { - lsp::OneOf::Left(location) => location, - lsp::OneOf::Right(_) => { - error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); - return None - } - }; - Some((lsp_symbol.name, lsp_symbol.kind, location)) - }).collect::>() - } - }).unwrap_or_default(); + }) => (adapter.clone(), language.clone(), server), - ( - adapter, - language, - worktree_id, - worktree_abs_path, - lsp_symbols, - ) - }), - ); - } - } + _ => continue, + }; + + requests.push( + server + .request::( + lsp::WorkspaceSymbolParams { + query: query.to_string(), + ..Default::default() + }, + ) + .log_err() + .map(move |response| { + let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response { + lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { + flat_responses.into_iter().map(|lsp_symbol| { + (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) + }).collect::>() + } + lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { + nested_responses.into_iter().filter_map(|lsp_symbol| { + let location = match lsp_symbol.location { + OneOf::Left(location) => location, + OneOf::Right(_) => { + error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); + return None + } + }; + Some((lsp_symbol.name, lsp_symbol.kind, location)) + }).collect::>() + } + }).unwrap_or_default(); + + ( + adapter, + language, + worktree_id, + worktree_abs_path, + lsp_symbols, + ) + }), + ); } cx.spawn_weak(|this, cx| async move { let responses = futures::future::join_all(requests).await; - let this = if let Some(this) = this.upgrade(&cx) { - this - } else { - return Ok(Default::default()); + let this = match this.upgrade(&cx) { + Some(this) => this, + None => return Ok(Vec::new()), }; + let symbols = this.read_with(&cx, |this, cx| { let mut symbols = Vec::new(); for ( @@ -3926,8 +4286,10 @@ impl Project { }, )); } + symbols }); + Ok(futures::future::join_all(symbols).await) }) } else if let Some(project_id) = self.remote_id() { @@ -4208,13 +4570,20 @@ impl Project { this.last_workspace_edits_by_language_server .remove(&lang_server.server_id()); }); - lang_server + + let result = lang_server .request::(lsp::ExecuteCommandParams { command: command.command, arguments: command.arguments.unwrap_or_default(), ..Default::default() }) - .await?; + .await; + + if let Err(err) = result { + // TODO: LSP ERROR + return Err(err); + } + return Ok(this.update(&mut cx, |this, _| { this.last_workspace_edits_by_language_server .remove(&lang_server.server_id()) @@ -4374,7 +4743,7 @@ impl Project { uri, version: None, }, - edits: edits.into_iter().map(lsp::OneOf::Left).collect(), + edits: edits.into_iter().map(OneOf::Left).collect(), }) })); } @@ -4444,8 +4813,8 @@ impl Project { let edits = this .update(cx, |this, cx| { let edits = op.edits.into_iter().map(|edit| match edit { - lsp::OneOf::Left(edit) => edit, - lsp::OneOf::Right(edit) => edit.text_edit, + OneOf::Left(edit) => edit, + OneOf::Right(edit) => edit.text_edit, }); this.edits_from_lsp( &buffer_to_edit, @@ -4543,6 +4912,61 @@ impl Project { ) } + pub fn inlay_hints( + &self, + buffer_handle: ModelHandle, + range: Range, + cx: &mut ModelContext, + ) -> Task>> { + let buffer = buffer_handle.read(cx); + let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); + let range_start = range.start; + let range_end = range.end; + let buffer_id = buffer.remote_id(); + let buffer_version = buffer.version().clone(); + let lsp_request = InlayHints { range }; + + if self.is_local() { + let lsp_request_task = self.request_lsp(buffer_handle.clone(), lsp_request, cx); + cx.spawn(|_, mut cx| async move { + buffer_handle + .update(&mut cx, |buffer, _| { + buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp]) + }) + .await + .context("waiting for inlay hint request range edits")?; + lsp_request_task.await.context("inlay hints LSP request") + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::InlayHints { + project_id, + buffer_id, + start: Some(serialize_anchor(&range_start)), + end: Some(serialize_anchor(&range_end)), + version: serialize_version(&buffer_version), + }; + cx.spawn(|project, cx| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + let hints_request_result = LspCommand::response_from_proto( + lsp_request, + response, + project, + buffer_handle.clone(), + cx, + ) + .await; + + hints_request_result.context("inlay hints proto response conversion") + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + #[allow(clippy::type_complexity)] pub fn search( &self, @@ -4782,10 +5206,20 @@ impl Project { return Ok(Default::default()); } - let response = language_server - .request::(lsp_params) - .await - .context("lsp request failed")?; + let result = language_server.request::(lsp_params).await; + let response = match result { + Ok(response) => response, + + Err(err) => { + log::warn!( + "Generic lsp request to {} failed: {}", + language_server.name(), + err + ); + return Err(err); + } + }; + request .response_from_lsp( response, @@ -5105,41 +5539,39 @@ impl Project { let abs_path = worktree_handle.read(cx).abs_path(); for server_id in &language_server_ids { - if let Some(server) = self.language_servers.get(server_id) { - if let LanguageServerState::Running { - server, - watched_paths, - .. - } = server - { - if let Some(watched_paths) = watched_paths.get(&worktree_id) { - let params = lsp::DidChangeWatchedFilesParams { - changes: changes - .iter() - .filter_map(|(path, _, change)| { - if !watched_paths.is_match(&path) { - return None; - } - let typ = match change { - PathChange::Loaded => return None, - PathChange::Added => lsp::FileChangeType::CREATED, - PathChange::Removed => lsp::FileChangeType::DELETED, - PathChange::Updated => lsp::FileChangeType::CHANGED, - PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, - }; - Some(lsp::FileEvent { - uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), - typ, - }) + if let Some(LanguageServerState::Running { + server, + watched_paths, + .. + }) = self.language_servers.get(server_id) + { + if let Some(watched_paths) = watched_paths.get(&worktree_id) { + let params = lsp::DidChangeWatchedFilesParams { + changes: changes + .iter() + .filter_map(|(path, _, change)| { + if !watched_paths.is_match(&path) { + return None; + } + let typ = match change { + PathChange::Loaded => return None, + PathChange::Added => lsp::FileChangeType::CREATED, + PathChange::Removed => lsp::FileChangeType::DELETED, + PathChange::Updated => lsp::FileChangeType::CHANGED, + PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, + }; + Some(lsp::FileEvent { + uri: lsp::Url::from_file_path(abs_path.join(path)).unwrap(), + typ, }) - .collect(), - }; + }) + .collect(), + }; - if !params.changes.is_empty() { - server - .notify::(params) - .log_err(); - } + if !params.changes.is_empty() { + server + .notify::(params) + .log_err(); } } } @@ -5699,6 +6131,29 @@ impl Project { }) } + async fn handle_expand_project_entry( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); + let worktree = this + .read_with(&cx, |this, cx| this.worktree_for_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("invalid request"))?; + worktree + .update(&mut cx, |worktree, cx| { + worktree + .as_local_mut() + .unwrap() + .expand_entry(entry_id, cx) + .ok_or_else(|| anyhow!("invalid entry")) + })? + .await?; + let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id()) as u64; + Ok(proto::ExpandProjectEntryResponse { worktree_scan_id }) + } + async fn handle_update_diagnostic_summary( this: ModelHandle, envelope: TypedEnvelope, @@ -6254,6 +6709,68 @@ impl Project { Ok(proto::OnTypeFormattingResponse { transaction }) } + async fn handle_inlay_hints( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id()?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade(cx)) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let buffer_version = deserialize_version(&envelope.payload.version); + + buffer + .update(&mut cx, |buffer, _| { + buffer.wait_for_version(buffer_version.clone()) + }) + .await + .with_context(|| { + format!( + "waiting for version {:?} for buffer {}", + buffer_version, + buffer.id() + ) + })?; + + let start = envelope + .payload + .start + .and_then(deserialize_anchor) + .context("missing range start")?; + let end = envelope + .payload + .end + .and_then(deserialize_anchor) + .context("missing range end")?; + let buffer_hints = this + .update(&mut cx, |project, cx| { + project.inlay_hints(buffer, start..end, cx) + }) + .await + .context("inlay hints fetch")?; + + Ok(this.update(&mut cx, |project, cx| { + InlayHints::response_to_proto(buffer_hints, project, sender_id, &buffer_version, cx) + })) + } + + async fn handle_refresh_inlay_hints( + this: ModelHandle, + _: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |_, cx| { + cx.emit(Event::RefreshInlays); + }); + Ok(proto::Ack {}) + } + async fn handle_lsp_command( this: ModelHandle, envelope: TypedEnvelope, @@ -6989,16 +7506,11 @@ impl Project { ) -> impl Iterator, &Arc)> { self.language_server_ids_for_buffer(buffer, cx) .into_iter() - .filter_map(|server_id| { - let server = self.language_servers.get(&server_id)?; - if let LanguageServerState::Running { + .filter_map(|server_id| match self.language_servers.get(&server_id)? { + LanguageServerState::Running { adapter, server, .. - } = server - { - Some((adapter, server)) - } else { - None - } + } => Some((adapter, server)), + _ => None, }) } @@ -7041,6 +7553,22 @@ impl Project { } } +fn glob_literal_prefix<'a>(glob: &'a str) -> &'a str { + let mut literal_end = 0; + for (i, part) in glob.split(path::MAIN_SEPARATOR).enumerate() { + if part.contains(&['*', '?', '{', '}']) { + break; + } else { + if i > 0 { + // Acount for separator prior to this part + literal_end += path::MAIN_SEPARATOR.len_utf8(); + } + literal_end += part.len(); + } + } + &glob[..literal_end] +} + impl WorktreeHandle { pub fn upgrade(&self, cx: &AppContext) -> Option> { match self { @@ -7152,11 +7680,10 @@ impl Entity for Project { .language_servers .drain() .map(|(_, server_state)| async { + use LanguageServerState::*; match server_state { - LanguageServerState::Running { server, .. } => server.shutdown()?.await, - LanguageServerState::Starting(starting_server) => { - starting_server.await?.shutdown()?.await - } + Running { server, .. } => server.shutdown()?.await, + Starting(task) => task.await?.shutdown()?.await, } }) .collect::>(); @@ -7188,6 +7715,26 @@ impl> From<(WorktreeId, P)> for ProjectPath { } } +impl ProjectLspAdapterDelegate { + fn new(project: &Project, cx: &ModelContext) -> Arc { + Arc::new(Self { + project: cx.handle(), + http_client: project.client.http_client(), + }) + } +} + +impl LspAdapterDelegate for ProjectLspAdapterDelegate { + fn show_notification(&self, message: &str, cx: &mut AppContext) { + self.project + .update(cx, |_, cx| cx.emit(Event::Notification(message.to_owned()))); + } + + fn http_client(&self) -> Arc { + self.http_client.clone() + } +} + fn split_operations( mut operations: Vec, ) -> impl Iterator> { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 3c23c30ab9..16e706a77e 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -535,8 +535,28 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon fs.insert_tree( "/the-root", json!({ - "a.rs": "", - "b.rs": "", + ".gitignore": "target\n", + "src": { + "a.rs": "", + "b.rs": "", + }, + "target": { + "x": { + "out": { + "x.rs": "" + } + }, + "y": { + "out": { + "y.rs": "", + } + }, + "z": { + "out": { + "z.rs": "" + } + } + } }), ) .await; @@ -550,11 +570,34 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon // Start the language server by opening a buffer with a compatible file extension. let _buffer = project .update(cx, |project, cx| { - project.open_local_buffer("/the-root/a.rs", cx) + project.open_local_buffer("/the-root/src/a.rs", cx) }) .await .unwrap(); + // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them. + project.read_with(cx, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + assert_eq!( + worktree + .read(cx) + .snapshot() + .entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_ignored)) + .collect::>(), + &[ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("src"), false), + (Path::new("src/a.rs"), false), + (Path::new("src/b.rs"), false), + (Path::new("target"), true), + ] + ); + }); + + let prev_read_dir_count = fs.read_dir_call_count(); + // Keep track of the FS events reported to the language server. let fake_server = fake_servers.next().await.unwrap(); let file_changes = Arc::new(Mutex::new(Vec::new())); @@ -565,12 +608,26 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon method: "workspace/didChangeWatchedFiles".to_string(), register_options: serde_json::to_value( lsp::DidChangeWatchedFilesRegistrationOptions { - watchers: vec![lsp::FileSystemWatcher { - glob_pattern: lsp::GlobPattern::String( - "/the-root/*.{rs,c}".to_string(), - ), - kind: None, - }], + watchers: vec![ + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + "/the-root/Cargo.toml".to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + "/the-root/src/*.{rs,c}".to_string(), + ), + kind: None, + }, + lsp::FileSystemWatcher { + glob_pattern: lsp::GlobPattern::String( + "/the-root/target/y/**/*.rs".to_string(), + ), + kind: None, + }, + ], }, ) .ok(), @@ -588,17 +645,51 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon }); cx.foreground().run_until_parked(); - assert_eq!(file_changes.lock().len(), 0); + assert_eq!(mem::take(&mut *file_changes.lock()), &[]); + assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4); + + // Now the language server has asked us to watch an ignored directory path, + // so we recursively load it. + project.read_with(cx, |project, cx| { + let worktree = project.worktrees(cx).next().unwrap(); + assert_eq!( + worktree + .read(cx) + .snapshot() + .entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_ignored)) + .collect::>(), + &[ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("src"), false), + (Path::new("src/a.rs"), false), + (Path::new("src/b.rs"), false), + (Path::new("target"), true), + (Path::new("target/x"), true), + (Path::new("target/y"), true), + (Path::new("target/y/out"), true), + (Path::new("target/y/out/y.rs"), true), + (Path::new("target/z"), true), + ] + ); + }); // Perform some file system mutations, two of which match the watched patterns, // and one of which does not. - fs.create_file("/the-root/c.rs".as_ref(), Default::default()) + fs.create_file("/the-root/src/c.rs".as_ref(), Default::default()) .await .unwrap(); - fs.create_file("/the-root/d.txt".as_ref(), Default::default()) + fs.create_file("/the-root/src/d.txt".as_ref(), Default::default()) .await .unwrap(); - fs.remove_file("/the-root/b.rs".as_ref(), Default::default()) + fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default()) + .await + .unwrap(); + fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default()) + .await + .unwrap(); + fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default()) .await .unwrap(); @@ -608,11 +699,15 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon &*file_changes.lock(), &[ lsp::FileEvent { - uri: lsp::Url::from_file_path("/the-root/b.rs").unwrap(), + uri: lsp::Url::from_file_path("/the-root/src/b.rs").unwrap(), typ: lsp::FileChangeType::DELETED, }, lsp::FileEvent { - uri: lsp::Url::from_file_path("/the-root/c.rs").unwrap(), + uri: lsp::Url::from_file_path("/the-root/src/c.rs").unwrap(), + typ: lsp::FileChangeType::CREATED, + }, + lsp::FileEvent { + uri: lsp::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(), typ: lsp::FileChangeType::CREATED, }, ] @@ -3846,6 +3941,14 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex ); } +#[test] +fn test_glob_literal_prefix() { + assert_eq!(glob_literal_prefix("**/*.js"), ""); + assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules"); + assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo"); + assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js"); +} + async fn search( project: &ModelHandle, query: SearchQuery, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 561da2c292..2c3c9d5304 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -5,7 +5,7 @@ use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context, Result}; use client::{proto, Client}; use clock::ReplicaId; -use collections::{HashMap, VecDeque}; +use collections::{HashMap, HashSet, VecDeque}; use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, Fs, LineEnding, @@ -67,7 +67,8 @@ pub enum Worktree { pub struct LocalWorktree { snapshot: LocalSnapshot, - path_changes_tx: channel::Sender<(Vec, barrier::Sender)>, + scan_requests_tx: channel::Sender, + path_prefixes_to_scan_tx: channel::Sender>, is_scanning: (watch::Sender, watch::Receiver), _background_scanner_task: Task<()>, share: Option, @@ -84,6 +85,11 @@ pub struct LocalWorktree { visible: bool, } +struct ScanRequest { + relative_paths: Vec>, + done: barrier::Sender, +} + pub struct RemoteWorktree { snapshot: Snapshot, background_snapshot: Arc>, @@ -214,6 +220,9 @@ pub struct LocalSnapshot { struct BackgroundScannerState { snapshot: LocalSnapshot, + scanned_dirs: HashSet, + path_prefixes_to_scan: HashSet>, + paths_to_scan: HashSet>, /// The ids of all of the entries that were removed from the snapshot /// as part of the current update. These entry ids may be re-used /// if the same inode is discovered at a new path, or if the given @@ -232,13 +241,6 @@ pub struct LocalRepositoryEntry { pub(crate) git_dir_path: Arc, } -impl LocalRepositoryEntry { - // Note that this path should be relative to the worktree root. - pub(crate) fn in_dot_git(&self, path: &Path) -> bool { - path.starts_with(self.git_dir_path.as_ref()) - } -} - impl Deref for LocalSnapshot { type Target = Snapshot; @@ -330,7 +332,8 @@ impl Worktree { ); } - let (path_changes_tx, path_changes_rx) = channel::unbounded(); + let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); + let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); cx.spawn_weak(|this, mut cx| async move { @@ -370,7 +373,8 @@ impl Worktree { fs, scan_states_tx, background, - path_changes_rx, + scan_requests_rx, + path_prefixes_to_scan_rx, ) .run(events) .await; @@ -381,7 +385,8 @@ impl Worktree { snapshot, is_scanning: watch::channel_with(true), share: None, - path_changes_tx, + scan_requests_tx, + path_prefixes_to_scan_tx, _background_scanner_task: background_scanner_task, diagnostics: Default::default(), diagnostic_summaries: Default::default(), @@ -867,27 +872,27 @@ impl LocalWorktree { path: &Path, cx: &mut ModelContext, ) -> Task)>> { - let handle = cx.handle(); let path = Arc::from(path); let abs_path = self.absolutize(&path); let fs = self.fs.clone(); - let snapshot = self.snapshot(); + let entry = self.refresh_entry(path.clone(), None, cx); - let mut index_task = None; - - if let Some(repo) = snapshot.repository_for_path(&path) { - let repo_path = repo.work_directory.relativize(self, &path).unwrap(); - if let Some(repo) = self.git_repositories.get(&*repo.work_directory) { - let repo = repo.repo_ptr.to_owned(); - index_task = Some( - cx.background() - .spawn(async move { repo.lock().load_index_text(&repo_path) }), - ); - } - } - - cx.spawn(|this, mut cx| async move { + cx.spawn(|this, cx| async move { let text = fs.load(&abs_path).await?; + let entry = entry.await?; + + let mut index_task = None; + let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot()); + if let Some(repo) = snapshot.repository_for_path(&path) { + let repo_path = repo.work_directory.relativize(&snapshot, &path).unwrap(); + if let Some(repo) = snapshot.git_repositories.get(&*repo.work_directory) { + let repo = repo.repo_ptr.clone(); + index_task = Some( + cx.background() + .spawn(async move { repo.lock().load_index_text(&repo_path) }), + ); + } + } let diff_base = if let Some(index_task) = index_task { index_task.await @@ -895,17 +900,10 @@ impl LocalWorktree { None }; - // Eagerly populate the snapshot with an updated entry for the loaded file - let entry = this - .update(&mut cx, |this, cx| { - this.as_local().unwrap().refresh_entry(path, None, cx) - }) - .await?; - Ok(( File { entry_id: entry.id, - worktree: handle, + worktree: this, path: entry.path, mtime: entry.mtime, is_local: true, @@ -983,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>, @@ -990,6 +1001,7 @@ impl LocalWorktree { cx: &mut ModelContext, ) -> Task> { 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 { @@ -1003,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::>>::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 }) } @@ -1039,14 +1072,10 @@ impl LocalWorktree { cx: &mut ModelContext, ) -> Option>> { let entry = self.entry_for_id(entry_id)?.clone(); - let abs_path = self.abs_path.clone(); + let abs_path = self.absolutize(&entry.path); let fs = self.fs.clone(); let delete = cx.background().spawn(async move { - let mut abs_path = fs.canonicalize(&abs_path).await?; - if entry.path.file_name().is_some() { - abs_path = abs_path.join(&entry.path); - } if entry.is_file() { fs.remove_file(&abs_path, Default::default()).await?; } else { @@ -1059,19 +1088,18 @@ impl LocalWorktree { ) .await?; } - anyhow::Ok(abs_path) + anyhow::Ok(entry.path) }); Some(cx.spawn(|this, mut cx| async move { - let abs_path = delete.await?; - let (tx, mut rx) = barrier::channel(); + let path = delete.await?; this.update(&mut cx, |this, _| { this.as_local_mut() .unwrap() - .path_changes_tx - .try_send((vec![abs_path], tx)) - })?; - rx.recv().await; + .refresh_entries_for_paths(vec![path]) + }) + .recv() + .await; Ok(()) })) } @@ -1135,34 +1163,48 @@ impl LocalWorktree { })) } + pub fn expand_entry( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Option>> { + let path = self.entry_for_id(entry_id)?.path.clone(); + let mut refresh = self.refresh_entries_for_paths(vec![path]); + Some(cx.background().spawn(async move { + refresh.next().await; + Ok(()) + })) + } + + pub fn refresh_entries_for_paths(&self, paths: Vec>) -> barrier::Receiver { + let (tx, rx) = barrier::channel(); + self.scan_requests_tx + .try_send(ScanRequest { + relative_paths: paths, + done: tx, + }) + .ok(); + rx + } + + pub fn add_path_prefix_to_scan(&self, path_prefix: Arc) { + self.path_prefixes_to_scan_tx.try_send(path_prefix).ok(); + } + fn refresh_entry( &self, path: Arc, old_path: Option>, cx: &mut ModelContext, ) -> Task> { - let fs = self.fs.clone(); - let abs_root_path = self.abs_path.clone(); - let path_changes_tx = self.path_changes_tx.clone(); + let paths = if let Some(old_path) = old_path.as_ref() { + vec![old_path.clone(), path.clone()] + } else { + vec![path.clone()] + }; + let mut refresh = self.refresh_entries_for_paths(paths); cx.spawn_weak(move |this, mut cx| async move { - let abs_path = fs.canonicalize(&abs_root_path).await?; - let mut paths = Vec::with_capacity(2); - paths.push(if path.file_name().is_some() { - abs_path.join(&path) - } else { - abs_path.clone() - }); - if let Some(old_path) = old_path { - paths.push(if old_path.file_name().is_some() { - abs_path.join(&old_path) - } else { - abs_path.clone() - }); - } - - let (tx, mut rx) = barrier::channel(); - path_changes_tx.try_send((paths, tx))?; - rx.recv().await; + refresh.recv().await; this.upgrade(&cx) .ok_or_else(|| anyhow!("worktree was dropped"))? .update(&mut cx, |this, _| { @@ -1331,7 +1373,7 @@ impl RemoteWorktree { self.completed_scan_id >= scan_id } - fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future> { + pub(crate) fn wait_for_snapshot(&mut self, scan_id: usize) -> impl Future> { let (tx, rx) = oneshot::channel(); if self.observed_snapshot(scan_id) { let _ = tx.send(()); @@ -1470,7 +1512,7 @@ impl Snapshot { break; } } - new_entries_by_path.push_tree(cursor.suffix(&()), &()); + new_entries_by_path.append(cursor.suffix(&()), &()); new_entries_by_path }; @@ -1568,7 +1610,7 @@ impl Snapshot { } pub fn visible_file_count(&self) -> usize { - self.entries_by_path.summary().visible_file_count + self.entries_by_path.summary().non_ignored_file_count } fn traverse_from_offset( @@ -1837,15 +1879,6 @@ impl LocalSnapshot { Some((path, self.git_repositories.get(&repo.work_directory_id())?)) } - pub(crate) fn repo_for_metadata( - &self, - path: &Path, - ) -> Option<(&ProjectEntryId, &LocalRepositoryEntry)> { - self.git_repositories - .iter() - .find(|(_, repo)| repo.in_dot_git(path)) - } - fn build_update( &self, project_id: u64, @@ -1981,57 +2014,6 @@ impl LocalSnapshot { entry } - #[must_use = "Changed paths must be used for diffing later"] - fn build_repo(&mut self, parent_path: Arc, fs: &dyn Fs) -> Option>> { - let abs_path = self.abs_path.join(&parent_path); - let work_dir: Arc = parent_path.parent().unwrap().into(); - - // Guard against repositories inside the repository metadata - if work_dir - .components() - .find(|component| component.as_os_str() == *DOT_GIT) - .is_some() - { - return None; - }; - - let work_dir_id = self - .entry_for_path(work_dir.clone()) - .map(|entry| entry.id)?; - - if self.git_repositories.get(&work_dir_id).is_some() { - return None; - } - - let repo = fs.open_repo(abs_path.as_path())?; - let work_directory = RepositoryWorkDirectory(work_dir.clone()); - - let repo_lock = repo.lock(); - - self.repository_entries.insert( - work_directory.clone(), - RepositoryEntry { - work_directory: work_dir_id.into(), - branch: repo_lock.branch_name().map(Into::into), - }, - ); - - let changed_paths = self.scan_statuses(repo_lock.deref(), &work_directory); - - drop(repo_lock); - - self.git_repositories.insert( - work_dir_id, - LocalRepositoryEntry { - git_dir_scan_id: 0, - repo_ptr: repo, - git_dir_path: parent_path.clone(), - }, - ); - - Some(changed_paths) - } - #[must_use = "Changed paths must be used for diffing later"] fn scan_statuses( &mut self, @@ -2098,11 +2080,18 @@ impl LocalSnapshot { ignore_stack } -} -impl LocalSnapshot { #[cfg(test)] - pub fn check_invariants(&self) { + pub(crate) fn expanded_entries(&self) -> impl Iterator { + self.entries_by_path + .cursor::<()>() + .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored)) + } + + #[cfg(test)] + pub fn check_invariants(&self, git_state: bool) { + use pretty_assertions::assert_eq; + assert_eq!( self.entries_by_path .cursor::<()>() @@ -2122,7 +2111,7 @@ impl LocalSnapshot { for entry in self.entries_by_path.cursor::<()>() { if entry.is_file() { assert_eq!(files.next().unwrap().inode, entry.inode); - if !entry.is_ignored { + if !entry.is_ignored && !entry.is_external { assert_eq!(visible_files.next().unwrap().inode, entry.inode); } } @@ -2132,7 +2121,11 @@ impl LocalSnapshot { assert!(visible_files.next().is_none()); let mut bfs_paths = Vec::new(); - let mut stack = vec![Path::new("")]; + let mut stack = self + .root_entry() + .map(|e| e.path.as_ref()) + .into_iter() + .collect::>(); while let Some(path) = stack.pop() { bfs_paths.push(path); let ix = stack.len(); @@ -2154,12 +2147,15 @@ impl LocalSnapshot { .collect::>(); assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter); - for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() { - let ignore_parent_path = ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap(); - assert!(self.entry_for_path(&ignore_parent_path).is_some()); - assert!(self - .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) - .is_some()); + if git_state { + for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() { + let ignore_parent_path = + ignore_parent_abs_path.strip_prefix(&self.abs_path).unwrap(); + assert!(self.entry_for_path(&ignore_parent_path).is_some()); + assert!(self + .entry_for_path(ignore_parent_path.join(&*GITIGNORE)) + .is_some()); + } } } @@ -2177,6 +2173,20 @@ 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 + .iter() + .any(|p| p.starts_with(&entry.path)) + || self + .path_prefixes_to_scan + .iter() + .any(|p| entry.path.starts_with(p)) + } + fn reuse_entry_id(&mut self, entry: &mut Entry) { if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) { entry.id = removed_entry_id; @@ -2187,17 +2197,24 @@ impl BackgroundScannerState { fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { self.reuse_entry_id(&mut entry); - self.snapshot.insert_entry(entry, fs) + let entry = self.snapshot.insert_entry(entry, fs); + if entry.path.file_name() == Some(&DOT_GIT) { + self.build_repository(entry.path.clone(), fs); + } + + #[cfg(test)] + self.snapshot.check_invariants(false); + + entry } - #[must_use = "Changed paths must be used for diffing later"] fn populate_dir( &mut self, - parent_path: Arc, + parent_path: &Arc, entries: impl IntoIterator, ignore: Option>, fs: &dyn Fs, - ) -> Option>> { + ) { let mut parent_entry = if let Some(parent_entry) = self .snapshot .entries_by_path @@ -2209,15 +2226,13 @@ impl BackgroundScannerState { "populating a directory {:?} that has been removed", parent_path ); - return None; + return; }; match parent_entry.kind { - EntryKind::PendingDir => { - parent_entry.kind = EntryKind::Dir; - } + EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir, EntryKind::Dir => {} - _ => return None, + _ => return, } if let Some(ignore) = ignore { @@ -2227,11 +2242,16 @@ impl BackgroundScannerState { .insert(abs_parent_path, (ignore, false)); } + self.scanned_dirs.insert(parent_entry.id); let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; let mut entries_by_id_edits = Vec::new(); + let mut dotgit_path = None; + + for entry in entries { + if entry.path.file_name() == Some(&DOT_GIT) { + dotgit_path = Some(entry.path.clone()); + } - for mut entry in entries { - self.reuse_entry_id(&mut entry); entries_by_id_edits.push(Edit::Insert(PathEntry { id: entry.id, path: entry.path.clone(), @@ -2246,10 +2266,15 @@ impl BackgroundScannerState { .edit(entries_by_path_edits, &()); self.snapshot.entries_by_id.edit(entries_by_id_edits, &()); - if parent_path.file_name() == Some(&DOT_GIT) { - return self.snapshot.build_repo(parent_path, fs); + if let Some(dotgit_path) = dotgit_path { + self.build_repository(dotgit_path, fs); } - None + if let Err(ix) = self.changed_paths.binary_search(parent_path) { + self.changed_paths.insert(ix, parent_path.clone()); + } + + #[cfg(test)] + self.snapshot.check_invariants(false); } fn remove_path(&mut self, path: &Path) { @@ -2259,7 +2284,7 @@ impl BackgroundScannerState { let mut cursor = self.snapshot.entries_by_path.cursor::(); new_entries = cursor.slice(&TraversalTarget::Path(path), Bias::Left, &()); removed_entries = cursor.slice(&TraversalTarget::PathSuccessor(path), Bias::Left, &()); - new_entries.push_tree(cursor.suffix(&()), &()); + new_entries.append(cursor.suffix(&()), &()); } self.snapshot.entries_by_path = new_entries; @@ -2284,6 +2309,140 @@ impl BackgroundScannerState { *needs_update = true; } } + + #[cfg(test)] + self.snapshot.check_invariants(false); + } + + fn reload_repositories(&mut self, changed_paths: &[Arc], fs: &dyn Fs) { + let scan_id = self.snapshot.scan_id; + + // Find each of the .git directories that contain any of the given paths. + let mut prev_dot_git_dir = None; + for changed_path in changed_paths { + let Some(dot_git_dir) = changed_path + .ancestors() + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) else { + continue; + }; + + // Avoid processing the same repository multiple times, if multiple paths + // within it have changed. + if prev_dot_git_dir == Some(dot_git_dir) { + continue; + } + prev_dot_git_dir = Some(dot_git_dir); + + // If there is already a repository for this .git directory, reload + // the status for all of its files. + let repository = self + .snapshot + .git_repositories + .iter() + .find_map(|(entry_id, repo)| { + (repo.git_dir_path.as_ref() == dot_git_dir).then(|| (*entry_id, repo.clone())) + }); + match repository { + None => { + self.build_repository(dot_git_dir.into(), fs); + } + Some((entry_id, repository)) => { + if repository.git_dir_scan_id == scan_id { + continue; + } + let Some(work_dir) = self + .snapshot + .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(); + + self.snapshot + .git_repositories + .update(&entry_id, |entry| entry.git_dir_scan_id = scan_id); + self.snapshot + .snapshot + .repository_entries + .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); + + let changed_paths = self.snapshot.scan_statuses(&*repository, &work_dir); + util::extend_sorted( + &mut self.changed_paths, + changed_paths, + usize::MAX, + Ord::cmp, + ) + } + } + } + + // Remove any git repositories whose .git entry no longer exists. + let mut snapshot = &mut self.snapshot; + let mut repositories = mem::take(&mut snapshot.git_repositories); + let mut repository_entries = mem::take(&mut snapshot.repository_entries); + repositories.retain(|work_directory_id, _| { + snapshot + .entry_for_id(*work_directory_id) + .map_or(false, |entry| { + snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() + }) + }); + repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some()); + snapshot.git_repositories = repositories; + snapshot.repository_entries = repository_entries; + } + + fn build_repository(&mut self, dot_git_path: Arc, fs: &dyn Fs) -> Option<()> { + log::info!("build git repository {:?}", dot_git_path); + + let work_dir_path: Arc = dot_git_path.parent().unwrap().into(); + + // Guard against repositories inside the repository metadata + if work_dir_path.iter().any(|component| component == *DOT_GIT) { + return None; + }; + + let work_dir_id = self + .snapshot + .entry_for_path(work_dir_path.clone()) + .map(|entry| entry.id)?; + + if self.snapshot.git_repositories.get(&work_dir_id).is_some() { + return None; + } + + let abs_path = self.snapshot.abs_path.join(&dot_git_path); + let repository = fs.open_repo(abs_path.as_path())?; + let work_directory = RepositoryWorkDirectory(work_dir_path.clone()); + + let repo_lock = repository.lock(); + self.snapshot.repository_entries.insert( + work_directory.clone(), + RepositoryEntry { + work_directory: work_dir_id.into(), + branch: repo_lock.branch_name().map(Into::into), + }, + ); + + let changed_paths = self + .snapshot + .scan_statuses(repo_lock.deref(), &work_directory); + drop(repo_lock); + + self.snapshot.git_repositories.insert( + work_dir_id, + LocalRepositoryEntry { + git_dir_scan_id: 0, + repo_ptr: repository, + git_dir_path: dot_git_path.clone(), + }, + ); + + util::extend_sorted(&mut self.changed_paths, changed_paths, usize::MAX, Ord::cmp); + Some(()) } } @@ -2570,12 +2729,27 @@ pub struct Entry { pub inode: u64, pub mtime: SystemTime, pub is_symlink: bool, + + /// Whether this entry is ignored by Git. + /// + /// We only scan ignored entries once the directory is expanded and + /// exclude them from searches. pub is_ignored: bool, + + /// Whether this entry's canonical path is outside of the worktree. + /// This means the entry is only accessible from the worktree root via a + /// symlink. + /// + /// We only scan entries outside of the worktree once the symlinked + /// directory is expanded. External entries are treated like gitignored + /// entries in that they are not included in searches. + pub is_external: bool, pub git_status: Option, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EntryKind { + UnloadedDir, PendingDir, Dir, File(CharBag), @@ -2624,16 +2798,17 @@ impl Entry { mtime: metadata.mtime, is_symlink: metadata.is_symlink, is_ignored: false, + is_external: false, git_status: None, } } pub fn is_dir(&self) -> bool { - matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir) + self.kind.is_dir() } pub fn is_file(&self) -> bool { - matches!(self.kind, EntryKind::File(_)) + self.kind.is_file() } pub fn git_status(&self) -> Option { @@ -2641,19 +2816,40 @@ impl Entry { } } +impl EntryKind { + pub fn is_dir(&self) -> bool { + matches!( + self, + EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir + ) + } + + pub fn is_unloaded(&self) -> bool { + matches!(self, EntryKind::UnloadedDir) + } + + pub fn is_file(&self) -> bool { + matches!(self, EntryKind::File(_)) + } +} + impl sum_tree::Item for Entry { type Summary = EntrySummary; fn summary(&self) -> Self::Summary { - let visible_count = if self.is_ignored { 0 } else { 1 }; + let non_ignored_count = if self.is_ignored || self.is_external { + 0 + } else { + 1 + }; let file_count; - let visible_file_count; + let non_ignored_file_count; if self.is_file() { file_count = 1; - visible_file_count = visible_count; + non_ignored_file_count = non_ignored_count; } else { file_count = 0; - visible_file_count = 0; + non_ignored_file_count = 0; } let mut statuses = GitStatuses::default(); @@ -2669,9 +2865,9 @@ impl sum_tree::Item for Entry { EntrySummary { max_path: self.path.clone(), count: 1, - visible_count, + non_ignored_count, file_count, - visible_file_count, + non_ignored_file_count, statuses, } } @@ -2689,9 +2885,9 @@ impl sum_tree::KeyedItem for Entry { pub struct EntrySummary { max_path: Arc, count: usize, - visible_count: usize, + non_ignored_count: usize, file_count: usize, - visible_file_count: usize, + non_ignored_file_count: usize, statuses: GitStatuses, } @@ -2700,9 +2896,9 @@ impl Default for EntrySummary { Self { max_path: Arc::from(Path::new("")), count: 0, - visible_count: 0, + non_ignored_count: 0, file_count: 0, - visible_file_count: 0, + non_ignored_file_count: 0, statuses: Default::default(), } } @@ -2714,9 +2910,9 @@ impl sum_tree::Summary for EntrySummary { fn add_summary(&mut self, rhs: &Self, _: &()) { self.max_path = rhs.max_path.clone(); self.count += rhs.count; - self.visible_count += rhs.visible_count; + self.non_ignored_count += rhs.non_ignored_count; self.file_count += rhs.file_count; - self.visible_file_count += rhs.visible_file_count; + self.non_ignored_file_count += rhs.non_ignored_file_count; self.statuses += rhs.statuses; } } @@ -2784,7 +2980,8 @@ struct BackgroundScanner { fs: Arc, status_updates_tx: UnboundedSender, executor: Arc, - refresh_requests_rx: channel::Receiver<(Vec, barrier::Sender)>, + scan_requests_rx: channel::Receiver, + path_prefixes_to_scan_rx: channel::Receiver>, next_entry_id: Arc, phase: BackgroundScannerPhase, } @@ -2803,17 +3000,22 @@ impl BackgroundScanner { fs: Arc, status_updates_tx: UnboundedSender, executor: Arc, - refresh_requests_rx: channel::Receiver<(Vec, barrier::Sender)>, + scan_requests_rx: channel::Receiver, + path_prefixes_to_scan_rx: channel::Receiver>, ) -> Self { Self { fs, status_updates_tx, executor, - refresh_requests_rx, + scan_requests_rx, + path_prefixes_to_scan_rx, next_entry_id, state: Mutex::new(BackgroundScannerState { prev_snapshot: snapshot.snapshot.clone(), snapshot, + scanned_dirs: Default::default(), + path_prefixes_to_scan: Default::default(), + paths_to_scan: Default::default(), removed_entry_ids: Default::default(), changed_paths: Default::default(), }), @@ -2823,7 +3025,7 @@ impl BackgroundScanner { async fn run( &mut self, - mut events_rx: Pin>>>, + mut fs_events_rx: Pin>>>, ) { use futures::FutureExt as _; @@ -2868,6 +3070,7 @@ impl BackgroundScanner { path: Arc::from(Path::new("")), ignore_stack, ancestor_inodes: TreeSet::from_ordered_entries(root_inode), + is_external: false, scan_queue: scan_job_tx.clone(), })) .unwrap(); @@ -2884,9 +3087,9 @@ impl BackgroundScanner { // For these events, update events cannot be as precise, because we didn't // have the previous state loaded yet. self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan; - if let Poll::Ready(Some(events)) = futures::poll!(events_rx.next()) { + if let Poll::Ready(Some(events)) = futures::poll!(fs_events_rx.next()) { let mut paths = events.into_iter().map(|e| e.path).collect::>(); - while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) { + while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) { paths.extend(more_events.into_iter().map(|e| e.path)); } self.process_events(paths).await; @@ -2898,17 +3101,36 @@ impl BackgroundScanner { select_biased! { // Process any path refresh requests from the worktree. Prioritize // these before handling changes reported by the filesystem. - request = self.refresh_requests_rx.recv().fuse() => { - let Ok((paths, barrier)) = request else { break }; - if !self.process_refresh_request(paths.clone(), barrier).await { + request = self.scan_requests_rx.recv().fuse() => { + let Ok(request) = request else { break }; + if !self.process_scan_request(request, false).await { return; } } - events = events_rx.next().fuse() => { + path_prefix = self.path_prefixes_to_scan_rx.recv().fuse() => { + let Ok(path_prefix) = path_prefix else { break }; + log::trace!("adding path prefix {:?}", path_prefix); + + let did_scan = self.forcibly_load_paths(&[path_prefix.clone()]).await; + if did_scan { + let abs_path = + { + let mut state = self.state.lock(); + state.path_prefixes_to_scan.insert(path_prefix.clone()); + state.snapshot.abs_path.join(&path_prefix) + }; + + if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() { + self.process_events(vec![abs_path]).await; + } + } + } + + events = fs_events_rx.next().fuse() => { let Some(events) = events else { break }; let mut paths = events.into_iter().map(|e| e.path).collect::>(); - while let Poll::Ready(Some(more_events)) = futures::poll!(events_rx.next()) { + while let Poll::Ready(Some(more_events)) = futures::poll!(fs_events_rx.next()) { paths.extend(more_events.into_iter().map(|e| e.path)); } self.process_events(paths.clone()).await; @@ -2917,56 +3139,157 @@ impl BackgroundScanner { } } - async fn process_refresh_request(&self, paths: Vec, barrier: barrier::Sender) -> bool { - self.reload_entries_for_paths(paths, None).await; - self.send_status_update(false, Some(barrier)) + async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool { + log::debug!("rescanning paths {:?}", request.relative_paths); + + request.relative_paths.sort_unstable(); + self.forcibly_load_paths(&request.relative_paths).await; + + let root_path = self.state.lock().snapshot.abs_path.clone(); + let root_canonical_path = match self.fs.canonicalize(&root_path).await { + Ok(path) => path, + Err(err) => { + log::error!("failed to canonicalize root path: {}", err); + return false; + } + }; + let abs_paths = request + .relative_paths + .iter() + .map(|path| { + if path.file_name().is_some() { + root_canonical_path.join(path) + } else { + root_canonical_path.clone() + } + }) + .collect::>(); + + self.reload_entries_for_paths( + root_path, + root_canonical_path, + &request.relative_paths, + abs_paths, + None, + ) + .await; + self.send_status_update(scanning, Some(request.done)) } - async fn process_events(&mut self, paths: Vec) { + async fn process_events(&mut self, mut abs_paths: Vec) { + let root_path = self.state.lock().snapshot.abs_path.clone(); + let root_canonical_path = match self.fs.canonicalize(&root_path).await { + Ok(path) => path, + Err(err) => { + log::error!("failed to canonicalize root path: {}", err); + return; + } + }; + + let mut relative_paths = Vec::with_capacity(abs_paths.len()); + abs_paths.sort_unstable(); + abs_paths.dedup_by(|a, b| a.starts_with(&b)); + abs_paths.retain(|abs_path| { + let snapshot = &self.state.lock().snapshot; + { + let relative_path: Arc = + if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { + path.into() + } else { + log::error!( + "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}", + ); + return false; + }; + + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { + snapshot + .entry_for_path(parent) + .map_or(false, |entry| entry.kind == EntryKind::Dir) + }); + if !parent_dir_is_loaded { + log::debug!("ignoring event {relative_path:?} within unloaded directory"); + return false; + } + + relative_paths.push(relative_path); + true + } + }); + + if relative_paths.is_empty() { + return; + } + + log::debug!("received fs events {:?}", relative_paths); + let (scan_job_tx, scan_job_rx) = channel::unbounded(); - let paths = self - .reload_entries_for_paths(paths, Some(scan_job_tx.clone())) - .await; + 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; - self.update_ignore_statuses().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(); - - if let Some(paths) = paths { - for path in paths { - self.reload_git_repo(&path, &mut *state, self.fs.as_ref()); - } + 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) { + state.scanned_dirs.remove(&entry_id); } - - let mut snapshot = &mut state.snapshot; - - let mut git_repositories = mem::take(&mut snapshot.git_repositories); - git_repositories.retain(|work_directory_id, _| { - snapshot - .entry_for_id(*work_directory_id) - .map_or(false, |entry| { - snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some() - }) - }); - snapshot.git_repositories = git_repositories; - - let mut git_repository_entries = mem::take(&mut snapshot.snapshot.repository_entries); - git_repository_entries.retain(|_, entry| { - snapshot - .git_repositories - .get(&entry.work_directory.0) - .is_some() - }); - snapshot.snapshot.repository_entries = git_repository_entries; - snapshot.completed_scan_id = snapshot.scan_id; } self.send_status_update(false, None); } + async fn forcibly_load_paths(&self, paths: &[Arc]) -> bool { + let (scan_job_tx, mut scan_job_rx) = channel::unbounded(); + { + let mut state = self.state.lock(); + let root_path = state.snapshot.abs_path.clone(); + for path in paths { + for ancestor in path.ancestors() { + if let Some(entry) = state.snapshot.entry_for_path(ancestor) { + if entry.kind == EntryKind::UnloadedDir { + let abs_path = root_path.join(ancestor); + let ignore_stack = + state.snapshot.ignore_stack_for_abs_path(&abs_path, true); + let ancestor_inodes = + state.snapshot.ancestor_inodes_for_path(&ancestor); + scan_job_tx + .try_send(ScanJob { + abs_path: abs_path.into(), + path: ancestor.into(), + ignore_stack, + scan_queue: scan_job_tx.clone(), + ancestor_inodes, + is_external: entry.is_external, + }) + .unwrap(); + state.paths_to_scan.insert(path.clone()); + break; + } + } + } + } + drop(scan_job_tx); + } + while let Some(job) = scan_job_rx.next().await { + self.scan_dir(&job).await.log_err(); + } + + mem::take(&mut self.state.lock().paths_to_scan).len() > 0 + } + async fn scan_dirs( &self, enable_progress_updates: bool, @@ -2995,9 +3318,9 @@ impl BackgroundScanner { select_biased! { // Process any path refresh requests before moving on to process // the scan queue, so that user operations are prioritized. - request = self.refresh_requests_rx.recv().fuse() => { - let Ok((paths, barrier)) = request else { break }; - if !self.process_refresh_request(paths, barrier).await { + request = self.scan_requests_rx.recv().fuse() => { + let Ok(request) = request else { break }; + if !self.process_scan_request(request, true).await { return; } } @@ -3062,8 +3385,8 @@ impl BackgroundScanner { } async fn scan_dir(&self, job: &ScanJob) -> Result<()> { - let mut new_entries: Vec = Vec::new(); - let mut new_jobs: Vec> = Vec::new(); + log::debug!("scan directory {:?}", job.path); + let mut ignore_stack = job.ignore_stack.clone(); let mut new_ignore = None; let (root_abs_path, root_char_bag, next_entry_id, repository) = { @@ -3078,6 +3401,9 @@ impl BackgroundScanner { ) }; + let mut root_canonical_path = None; + let mut new_entries: Vec = Vec::new(); + let mut new_jobs: Vec> = Vec::new(); let mut child_paths = self.fs.read_dir(&job.abs_path).await?; while let Some(child_abs_path) = child_paths.next().await { let child_abs_path: Arc = match child_abs_path { @@ -3127,7 +3453,7 @@ impl BackgroundScanner { ignore_stack.is_abs_path_ignored(&entry_abs_path, entry.is_dir()); if entry.is_dir() { - if let Some(job) = new_jobs.next().expect("Missing scan job for entry") { + if let Some(job) = new_jobs.next().expect("missing scan job for entry") { job.ignore_stack = if entry.is_ignored { IgnoreStack::all() } else { @@ -3145,9 +3471,41 @@ impl BackgroundScanner { root_char_bag, ); + if job.is_external { + child_entry.is_external = true; + } else if child_metadata.is_symlink { + let canonical_path = match self.fs.canonicalize(&child_abs_path).await { + Ok(path) => path, + Err(err) => { + log::error!( + "error reading target of symlink {:?}: {:?}", + child_abs_path, + err + ); + continue; + } + }; + + // lazily canonicalize the root path in order to determine if + // symlinks point outside of the worktree. + let root_canonical_path = match &root_canonical_path { + Some(path) => path, + None => match self.fs.canonicalize(&root_abs_path).await { + Ok(path) => root_canonical_path.insert(path), + Err(err) => { + log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err); + continue; + } + }, + }; + + if !canonical_path.starts_with(root_canonical_path) { + child_entry.is_external = true; + } + } + if child_entry.is_dir() { - let is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); - child_entry.is_ignored = is_ignored; + child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); // Avoid recursing until crash in the case of a recursive symlink if !job.ancestor_inodes.contains(&child_entry.inode) { @@ -3157,7 +3515,8 @@ impl BackgroundScanner { new_jobs.push(Some(ScanJob { abs_path: child_abs_path, path: child_path, - ignore_stack: if is_ignored { + is_external: child_entry.is_external, + ignore_stack: if child_entry.is_ignored { IgnoreStack::all() } else { ignore_stack.clone() @@ -3187,48 +3546,51 @@ impl BackgroundScanner { new_entries.push(child_entry); } - { - let mut state = self.state.lock(); - let changed_paths = - state.populate_dir(job.path.clone(), new_entries, new_ignore, self.fs.as_ref()); - if let Err(ix) = state.changed_paths.binary_search(&job.path) { - state.changed_paths.insert(ix, job.path.clone()); - } - if let Some(changed_paths) = changed_paths { - util::extend_sorted( - &mut state.changed_paths, - changed_paths, - usize::MAX, - Ord::cmp, - ) - } - } - - for new_job in new_jobs { - if let Some(new_job) = new_job { - job.scan_queue.send(new_job).await.unwrap(); + let mut state = self.state.lock(); + let mut new_jobs = new_jobs.into_iter(); + for entry in &mut new_entries { + state.reuse_entry_id(entry); + + if entry.is_dir() { + let new_job = new_jobs.next().expect("missing scan job for entry"); + if state.should_scan_directory(&entry) { + if let Some(new_job) = new_job { + job.scan_queue + .try_send(new_job) + .expect("channel is unbounded"); + } + } else { + log::debug!("defer scanning directory {:?}", entry.path); + entry.kind = EntryKind::UnloadedDir; + } } } + assert!(new_jobs.next().is_none()); + state.populate_dir(&job.path, new_entries, new_ignore, self.fs.as_ref()); Ok(()) } async fn reload_entries_for_paths( &self, - mut abs_paths: Vec, + root_abs_path: Arc, + root_canonical_path: PathBuf, + relative_paths: &[Arc], + abs_paths: Vec, scan_queue_tx: Option>, - ) -> Option>> { - let doing_recursive_update = scan_queue_tx.is_some(); - - abs_paths.sort_unstable(); - abs_paths.dedup_by(|a, b| a.starts_with(&b)); - - let root_abs_path = self.state.lock().snapshot.abs_path.clone(); - let root_canonical_path = self.fs.canonicalize(&root_abs_path).await.log_err()?; + ) { let metadata = futures::future::join_all( abs_paths .iter() - .map(|abs_path| self.fs.metadata(&abs_path)) + .map(|abs_path| async move { + let metadata = self.fs.metadata(&abs_path).await?; + if let Some(metadata) = metadata { + let canonical_path = self.fs.canonicalize(&abs_path).await?; + anyhow::Ok(Some((metadata, canonical_path))) + } else { + Ok(None) + } + }) .collect::>(), ) .await; @@ -3236,6 +3598,7 @@ impl BackgroundScanner { let mut state = self.state.lock(); let snapshot = &mut state.snapshot; let is_idle = snapshot.completed_scan_id == snapshot.scan_id; + let doing_recursive_update = scan_queue_tx.is_some(); snapshot.scan_id += 1; if is_idle && !doing_recursive_update { snapshot.completed_scan_id = snapshot.scan_id; @@ -3244,40 +3607,29 @@ impl BackgroundScanner { // Remove any entries for paths that no longer exist or are being recursively // refreshed. Do this before adding any new entries, so that renames can be // detected regardless of the order of the paths. - let mut event_paths = Vec::>::with_capacity(abs_paths.len()); - let mut event_metadata = Vec::<_>::with_capacity(abs_paths.len()); - for (abs_path, metadata) in abs_paths.iter().zip(metadata.iter()) { - if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { - if matches!(metadata, Ok(None)) || doing_recursive_update { - state.remove_path(path); - } - event_paths.push(path.into()); - event_metadata.push(metadata); - } else { - log::error!( - "unexpected event {:?} for root path {:?}", - abs_path, - root_canonical_path - ); + for (path, metadata) in relative_paths.iter().zip(metadata.iter()) { + if matches!(metadata, Ok(None)) || doing_recursive_update { + log::trace!("remove path {:?}", path); + state.remove_path(path); } } - for (path, metadata) in event_paths.iter().cloned().zip(event_metadata.into_iter()) { + for (path, metadata) in relative_paths.iter().zip(metadata.iter()) { let abs_path: Arc = root_abs_path.join(&path).into(); - match metadata { - Ok(Some(metadata)) => { + Ok(Some((metadata, canonical_path))) => { let ignore_stack = state .snapshot .ignore_stack_for_abs_path(&abs_path, metadata.is_dir); let mut fs_entry = Entry::new( path.clone(), - &metadata, + metadata, self.next_entry_id.as_ref(), state.snapshot.root_char_bag, ); fs_entry.is_ignored = ignore_stack.is_all(); + fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path); if !fs_entry.is_ignored { if !fs_entry.is_dir() { @@ -3296,22 +3648,28 @@ impl BackgroundScanner { } } - 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, - ignore_stack, - ancestor_inodes, - 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); @@ -3325,12 +3683,10 @@ impl BackgroundScanner { util::extend_sorted( &mut state.changed_paths, - event_paths.iter().cloned(), + relative_paths.iter().cloned(), usize::MAX, Ord::cmp, ); - - Some(event_paths) } fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> { @@ -3355,78 +3711,7 @@ impl BackgroundScanner { Some(()) } - fn reload_git_repo( - &self, - path: &Path, - state: &mut BackgroundScannerState, - fs: &dyn Fs, - ) -> Option<()> { - let scan_id = state.snapshot.scan_id; - - if path - .components() - .any(|component| component.as_os_str() == *DOT_GIT) - { - let (entry_id, repo_ptr) = { - let Some((entry_id, repo)) = state.snapshot.repo_for_metadata(&path) else { - let dot_git_dir = path.ancestors() - .skip_while(|ancestor| ancestor.file_name() != Some(&*DOT_GIT)) - .next()?; - - let changed_paths = state.snapshot.build_repo(dot_git_dir.into(), fs); - if let Some(changed_paths) = changed_paths { - util::extend_sorted( - &mut state.changed_paths, - changed_paths, - usize::MAX, - Ord::cmp, - ); - } - - return None; - }; - if repo.git_dir_scan_id == scan_id { - return None; - } - - (*entry_id, repo.repo_ptr.to_owned()) - }; - - let work_dir = state - .snapshot - .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone()))?; - - let repo = repo_ptr.lock(); - repo.reload_index(); - let branch = repo.branch_name(); - - state.snapshot.git_repositories.update(&entry_id, |entry| { - entry.git_dir_scan_id = scan_id; - }); - - state - .snapshot - .snapshot - .repository_entries - .update(&work_dir, |entry| { - entry.branch = branch.map(Into::into); - }); - - let changed_paths = state.snapshot.scan_statuses(repo.deref(), &work_dir); - - util::extend_sorted( - &mut state.changed_paths, - changed_paths, - usize::MAX, - Ord::cmp, - ) - } - - Some(()) - } - - async fn update_ignore_statuses(&self) { + async fn update_ignore_statuses(&self, scan_job_tx: Sender) { use futures::FutureExt as _; let mut snapshot = self.state.lock().snapshot.clone(); @@ -3474,6 +3759,7 @@ impl BackgroundScanner { abs_path: parent_abs_path, ignore_stack, ignore_queue: ignore_queue_tx.clone(), + scan_queue: scan_job_tx.clone(), })) .unwrap(); } @@ -3487,9 +3773,9 @@ impl BackgroundScanner { select_biased! { // Process any path refresh requests before moving on to process // the queue of ignore statuses. - request = self.refresh_requests_rx.recv().fuse() => { - let Ok((paths, barrier)) = request else { break }; - if !self.process_refresh_request(paths, barrier).await { + request = self.scan_requests_rx.recv().fuse() => { + let Ok(request) = request else { break }; + if !self.process_scan_request(request, true).await { return; } } @@ -3508,6 +3794,8 @@ impl BackgroundScanner { } async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { + log::trace!("update ignore status {:?}", job.abs_path); + let mut ignore_stack = job.ignore_stack; if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) { ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); @@ -3518,7 +3806,7 @@ impl BackgroundScanner { let path = job.abs_path.strip_prefix(&snapshot.abs_path).unwrap(); for mut entry in snapshot.child_entries(path).cloned() { let was_ignored = entry.is_ignored; - let abs_path = snapshot.abs_path().join(&entry.path); + let abs_path: Arc = snapshot.abs_path().join(&entry.path).into(); entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir()); if entry.is_dir() { let child_ignore_stack = if entry.is_ignored { @@ -3526,11 +3814,33 @@ impl BackgroundScanner { } else { ignore_stack.clone() }; + + // Scan any directories that were previously ignored and weren't + // previously scanned. + if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { + let state = self.state.lock(); + if state.should_scan_directory(&entry) { + job.scan_queue + .try_send(ScanJob { + abs_path: abs_path.clone(), + path: entry.path.clone(), + ignore_stack: child_ignore_stack.clone(), + scan_queue: job.scan_queue.clone(), + ancestor_inodes: state + .snapshot + .ancestor_inodes_for_path(&entry.path), + is_external: false, + }) + .unwrap(); + } + } + job.ignore_queue .send(UpdateIgnoreStatusJob { - abs_path: abs_path.into(), + abs_path: abs_path.clone(), ignore_stack: child_ignore_stack, ignore_queue: job.ignore_queue.clone(), + scan_queue: job.scan_queue.clone(), }) .await .unwrap(); @@ -3575,6 +3885,7 @@ impl BackgroundScanner { let mut changes = Vec::new(); let mut old_paths = old_snapshot.entries_by_path.cursor::(); let mut new_paths = new_snapshot.entries_by_path.cursor::(); + let mut last_newly_loaded_dir_path = None; old_paths.next(&()); new_paths.next(&()); for path in event_paths { @@ -3622,20 +3933,33 @@ impl BackgroundScanner { changes.push((old_entry.path.clone(), old_entry.id, Removed)); changes.push((new_entry.path.clone(), new_entry.id, Added)); } else if old_entry != new_entry { - changes.push((new_entry.path.clone(), new_entry.id, Updated)); + if old_entry.kind.is_unloaded() { + last_newly_loaded_dir_path = Some(&new_entry.path); + changes.push(( + new_entry.path.clone(), + new_entry.id, + Loaded, + )); + } else { + changes.push(( + new_entry.path.clone(), + new_entry.id, + Updated, + )); + } } old_paths.next(&()); new_paths.next(&()); } Ordering::Greater => { + let is_newly_loaded = self.phase == InitialScan + || last_newly_loaded_dir_path + .as_ref() + .map_or(false, |dir| new_entry.path.starts_with(&dir)); changes.push(( new_entry.path.clone(), new_entry.id, - if self.phase == InitialScan { - Loaded - } else { - Added - }, + if is_newly_loaded { Loaded } else { Added }, )); new_paths.next(&()); } @@ -3646,14 +3970,14 @@ impl BackgroundScanner { old_paths.next(&()); } (None, Some(new_entry)) => { + let is_newly_loaded = self.phase == InitialScan + || last_newly_loaded_dir_path + .as_ref() + .map_or(false, |dir| new_entry.path.starts_with(&dir)); changes.push(( new_entry.path.clone(), new_entry.id, - if self.phase == InitialScan { - Loaded - } else { - Added - }, + if is_newly_loaded { Loaded } else { Added }, )); new_paths.next(&()); } @@ -3695,12 +4019,14 @@ struct ScanJob { ignore_stack: Arc, scan_queue: Sender, ancestor_inodes: TreeSet, + is_external: bool, } struct UpdateIgnoreStatusJob { abs_path: Arc, ignore_stack: Arc, ignore_queue: Sender, + scan_queue: Sender, } pub trait WorktreeHandle { @@ -3754,9 +4080,9 @@ impl WorktreeHandle for ModelHandle { struct TraversalProgress<'a> { max_path: &'a Path, count: usize, - visible_count: usize, + non_ignored_count: usize, file_count: usize, - visible_file_count: usize, + non_ignored_file_count: usize, } impl<'a> TraversalProgress<'a> { @@ -3764,8 +4090,8 @@ impl<'a> TraversalProgress<'a> { match (include_ignored, include_dirs) { (true, true) => self.count, (true, false) => self.file_count, - (false, true) => self.visible_count, - (false, false) => self.visible_file_count, + (false, true) => self.non_ignored_count, + (false, false) => self.non_ignored_file_count, } } } @@ -3774,9 +4100,9 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> { fn add_summary(&mut self, summary: &'a EntrySummary, _: &()) { self.max_path = summary.max_path.as_ref(); self.count += summary.count; - self.visible_count += summary.visible_count; + self.non_ignored_count += summary.non_ignored_count; self.file_count += summary.file_count; - self.visible_file_count += summary.visible_file_count; + self.non_ignored_file_count += summary.non_ignored_file_count; } } @@ -3785,9 +4111,9 @@ impl<'a> Default for TraversalProgress<'a> { Self { max_path: Path::new(""), count: 0, - visible_count: 0, + non_ignored_count: 0, file_count: 0, - visible_file_count: 0, + non_ignored_file_count: 0, } } } @@ -3982,6 +4308,7 @@ impl<'a> From<&'a Entry> for proto::Entry { mtime: Some(entry.mtime.into()), is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, + is_external: entry.is_external, git_status: entry.git_status.map(|status| status.to_proto()), } } @@ -4008,6 +4335,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { mtime: mtime.into(), is_symlink: entry.is_symlink, is_ignored: entry.is_ignored, + is_external: entry.is_external, git_status: GitFileStatus::from_proto(entry.git_status), }) } else { diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 3abf660282..6f5b363509 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1,6 +1,6 @@ use crate::{ worktree::{Event, Snapshot, WorktreeHandle}, - EntryKind, PathChange, Worktree, + Entry, EntryKind, PathChange, Worktree, }; use anyhow::Result; use client::Client; @@ -8,12 +8,14 @@ use fs::{repository::GitFileStatus, FakeFs, Fs, RealFs, RemoveOptions}; use git::GITIGNORE; use gpui::{executor::Deterministic, ModelContext, Task, TestAppContext}; use parking_lot::Mutex; +use postage::stream::Stream; use pretty_assertions::assert_eq; use rand::prelude::*; use serde_json::json; use std::{ env, fmt::Write, + mem, path::{Path, PathBuf}, sync::Arc, }; @@ -34,11 +36,8 @@ async fn test_traversal(cx: &mut TestAppContext) { ) .await; - let http_client = FakeHttpClient::with_404_response(); - let client = cx.read(|cx| Client::new(http_client, cx)); - let tree = Worktree::local( - client, + build_client(cx), Path::new("/root"), true, fs, @@ -107,11 +106,8 @@ async fn test_descendent_entries(cx: &mut TestAppContext) { ) .await; - let http_client = FakeHttpClient::with_404_response(); - let client = cx.read(|cx| Client::new(http_client, cx)); - let tree = Worktree::local( - client, + build_client(cx), Path::new("/root"), true, fs, @@ -154,7 +150,18 @@ async fn test_descendent_entries(cx: &mut TestAppContext) { .collect::>(), vec![Path::new("g"), Path::new("g/h"),] ); + }); + // Expand gitignored directory. + tree.read_with(cx, |tree, _| { + tree.as_local() + .unwrap() + .refresh_entries_for_paths(vec![Path::new("i/j").into()]) + }) + .recv() + .await; + + tree.read_with(cx, |tree, _| { assert_eq!( tree.descendent_entries(false, false, Path::new("i")) .map(|entry| entry.path.as_ref()) @@ -196,9 +203,8 @@ async fn test_circular_symlinks(executor: Arc, cx: &mut TestAppCo fs.insert_symlink("/root/lib/a/lib", "..".into()).await; fs.insert_symlink("/root/lib/b/lib", "..".into()).await; - let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let tree = Worktree::local( - client, + build_client(cx), Path::new("/root"), true, fs.clone(), @@ -257,32 +263,490 @@ async fn test_circular_symlinks(executor: Arc, cx: &mut TestAppCo } #[gpui::test] -async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { - // .gitignores are handled explicitly by Zed and do not use the git - // machinery that the git_tests module checks - let parent_dir = temp_tree(json!({ - ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n", - "tree": { - ".git": {}, - ".gitignore": "ignored-dir\n", - "tracked-dir": { - "tracked-file1": "", - "ancestor-ignored-file1": "", +async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + "dir1": { + "deps": { + // symlinks here + }, + "src": { + "a.rs": "", + "b.rs": "", + }, }, - "ignored-dir": { - "ignored-file1": "" + "dir2": { + "src": { + "c.rs": "", + "d.rs": "", + } + }, + "dir3": { + "deps": {}, + "src": { + "e.rs": "", + "f.rs": "", + }, } - } - })); - let dir = parent_dir.path().join("tree"); + }), + ) + .await; - let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + // These symlinks point to directories outside of the worktree's root, dir1. + fs.insert_symlink("/root/dir1/deps/dep-dir2", "../../dir2".into()) + .await; + fs.insert_symlink("/root/dir1/deps/dep-dir3", "../../dir3".into()) + .await; let tree = Worktree::local( - client, - dir.as_path(), + build_client(cx), + Path::new("/root/dir1"), true, - Arc::new(RealFs), + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let tree_updates = Arc::new(Mutex::new(Vec::new())); + tree.update(cx, |_, cx| { + let tree_updates = tree_updates.clone(); + cx.subscribe(&tree, move |_, _, event, _| { + if let Event::UpdatedEntries(update) = event { + tree_updates.lock().extend( + update + .iter() + .map(|(path, _, change)| (path.clone(), *change)), + ); + } + }) + .detach(); + }); + + // The symlinked directories are not scanned by default. + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_external)) + .collect::>(), + vec![ + (Path::new(""), false), + (Path::new("deps"), false), + (Path::new("deps/dep-dir2"), true), + (Path::new("deps/dep-dir3"), true), + (Path::new("src"), false), + (Path::new("src/a.rs"), false), + (Path::new("src/b.rs"), false), + ] + ); + + assert_eq!( + tree.entry_for_path("deps/dep-dir2").unwrap().kind, + EntryKind::UnloadedDir + ); + }); + + // Expand one of the symlinked directories. + tree.read_with(cx, |tree, _| { + tree.as_local() + .unwrap() + .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3").into()]) + }) + .recv() + .await; + + // The expanded directory's contents are loaded. Subdirectories are + // not scanned yet. + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_external)) + .collect::>(), + vec![ + (Path::new(""), false), + (Path::new("deps"), false), + (Path::new("deps/dep-dir2"), true), + (Path::new("deps/dep-dir3"), true), + (Path::new("deps/dep-dir3/deps"), true), + (Path::new("deps/dep-dir3/src"), true), + (Path::new("src"), false), + (Path::new("src/a.rs"), false), + (Path::new("src/b.rs"), false), + ] + ); + }); + assert_eq!( + mem::take(&mut *tree_updates.lock()), + &[ + (Path::new("deps/dep-dir3").into(), PathChange::Loaded), + (Path::new("deps/dep-dir3/deps").into(), PathChange::Loaded), + (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded) + ] + ); + + // Expand a subdirectory of one of the symlinked directories. + tree.read_with(cx, |tree, _| { + tree.as_local() + .unwrap() + .refresh_entries_for_paths(vec![Path::new("deps/dep-dir3/src").into()]) + }) + .recv() + .await; + + // The expanded subdirectory's contents are loaded. + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_external)) + .collect::>(), + vec![ + (Path::new(""), false), + (Path::new("deps"), false), + (Path::new("deps/dep-dir2"), true), + (Path::new("deps/dep-dir3"), true), + (Path::new("deps/dep-dir3/deps"), true), + (Path::new("deps/dep-dir3/src"), true), + (Path::new("deps/dep-dir3/src/e.rs"), true), + (Path::new("deps/dep-dir3/src/f.rs"), true), + (Path::new("src"), false), + (Path::new("src/a.rs"), false), + (Path::new("src/b.rs"), false), + ] + ); + }); + + assert_eq!( + mem::take(&mut *tree_updates.lock()), + &[ + (Path::new("deps/dep-dir3/src").into(), PathChange::Loaded), + ( + Path::new("deps/dep-dir3/src/e.rs").into(), + PathChange::Loaded + ), + ( + Path::new("deps/dep-dir3/src/f.rs").into(), + PathChange::Loaded + ) + ] + ); +} + +#[gpui::test] +async fn test_open_gitignored_files(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + ".gitignore": "node_modules\n", + "one": { + "node_modules": { + "a": { + "a1.js": "a1", + "a2.js": "a2", + }, + "b": { + "b1.js": "b1", + "b2.js": "b2", + }, + "c": { + "c1.js": "c1", + "c2.js": "c2", + } + }, + }, + "two": { + "x.js": "", + "y.js": "", + }, + }), + ) + .await; + + let tree = Worktree::local( + build_client(cx), + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_ignored)) + .collect::>(), + vec![ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("one"), false), + (Path::new("one/node_modules"), true), + (Path::new("two"), false), + (Path::new("two/x.js"), false), + (Path::new("two/y.js"), false), + ] + ); + }); + + // Open a file that is nested inside of a gitignored directory that + // has not yet been expanded. + let prev_read_dir_count = fs.read_dir_call_count(); + let buffer = tree + .update(cx, |tree, cx| { + tree.as_local_mut() + .unwrap() + .load_buffer(0, "one/node_modules/b/b1.js".as_ref(), cx) + }) + .await + .unwrap(); + + tree.read_with(cx, |tree, cx| { + assert_eq!( + tree.entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_ignored)) + .collect::>(), + vec![ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("one"), false), + (Path::new("one/node_modules"), true), + (Path::new("one/node_modules/a"), true), + (Path::new("one/node_modules/b"), true), + (Path::new("one/node_modules/b/b1.js"), true), + (Path::new("one/node_modules/b/b2.js"), true), + (Path::new("one/node_modules/c"), true), + (Path::new("two"), false), + (Path::new("two/x.js"), false), + (Path::new("two/y.js"), false), + ] + ); + + assert_eq!( + buffer.read(cx).file().unwrap().path().as_ref(), + Path::new("one/node_modules/b/b1.js") + ); + + // Only the newly-expanded directories are scanned. + assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2); + }); + + // Open another file in a different subdirectory of the same + // gitignored directory. + let prev_read_dir_count = fs.read_dir_call_count(); + let buffer = tree + .update(cx, |tree, cx| { + tree.as_local_mut() + .unwrap() + .load_buffer(0, "one/node_modules/a/a2.js".as_ref(), cx) + }) + .await + .unwrap(); + + tree.read_with(cx, |tree, cx| { + assert_eq!( + tree.entries(true) + .map(|entry| (entry.path.as_ref(), entry.is_ignored)) + .collect::>(), + vec![ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("one"), false), + (Path::new("one/node_modules"), true), + (Path::new("one/node_modules/a"), true), + (Path::new("one/node_modules/a/a1.js"), true), + (Path::new("one/node_modules/a/a2.js"), true), + (Path::new("one/node_modules/b"), true), + (Path::new("one/node_modules/b/b1.js"), true), + (Path::new("one/node_modules/b/b2.js"), true), + (Path::new("one/node_modules/c"), true), + (Path::new("two"), false), + (Path::new("two/x.js"), false), + (Path::new("two/y.js"), false), + ] + ); + + assert_eq!( + buffer.read(cx).file().unwrap().path().as_ref(), + Path::new("one/node_modules/a/a2.js") + ); + + // Only the newly-expanded directory is scanned. + assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1); + }); + + // No work happens when files and directories change within an unloaded directory. + let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count(); + fs.create_dir("/root/one/node_modules/c/lib".as_ref()) + .await + .unwrap(); + cx.foreground().run_until_parked(); + assert_eq!( + fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count, + 0 + ); +} + +#[gpui::test] +async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + ".gitignore": "node_modules\n", + "a": { + "a.js": "", + }, + "b": { + "b.js": "", + }, + "node_modules": { + "c": { + "c.js": "", + }, + "d": { + "d.js": "", + "e": { + "e1.js": "", + "e2.js": "", + }, + "f": { + "f1.js": "", + "f2.js": "", + } + }, + }, + }), + ) + .await; + + let tree = Worktree::local( + build_client(cx), + Path::new("/root"), + true, + fs.clone(), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + // Open a file within the gitignored directory, forcing some of its + // subdirectories to be read, but not all. + let read_dir_count_1 = fs.read_dir_call_count(); + tree.read_with(cx, |tree, _| { + tree.as_local() + .unwrap() + .refresh_entries_for_paths(vec![Path::new("node_modules/d/d.js").into()]) + }) + .recv() + .await; + + // Those subdirectories are now loaded. + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true) + .map(|e| (e.path.as_ref(), e.is_ignored)) + .collect::>(), + &[ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("a"), false), + (Path::new("a/a.js"), false), + (Path::new("b"), false), + (Path::new("b/b.js"), false), + (Path::new("node_modules"), true), + (Path::new("node_modules/c"), true), + (Path::new("node_modules/d"), true), + (Path::new("node_modules/d/d.js"), true), + (Path::new("node_modules/d/e"), true), + (Path::new("node_modules/d/f"), true), + ] + ); + }); + let read_dir_count_2 = fs.read_dir_call_count(); + assert_eq!(read_dir_count_2 - read_dir_count_1, 2); + + // Update the gitignore so that node_modules is no longer ignored, + // but a subdirectory is ignored + fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default()) + .await + .unwrap(); + cx.foreground().run_until_parked(); + + // All of the directories that are no longer ignored are now loaded. + tree.read_with(cx, |tree, _| { + assert_eq!( + tree.entries(true) + .map(|e| (e.path.as_ref(), e.is_ignored)) + .collect::>(), + &[ + (Path::new(""), false), + (Path::new(".gitignore"), false), + (Path::new("a"), false), + (Path::new("a/a.js"), false), + (Path::new("b"), false), + (Path::new("b/b.js"), false), + // This directory is no longer ignored + (Path::new("node_modules"), false), + (Path::new("node_modules/c"), false), + (Path::new("node_modules/c/c.js"), false), + (Path::new("node_modules/d"), false), + (Path::new("node_modules/d/d.js"), false), + // This subdirectory is now ignored + (Path::new("node_modules/d/e"), true), + (Path::new("node_modules/d/f"), false), + (Path::new("node_modules/d/f/f1.js"), false), + (Path::new("node_modules/d/f/f2.js"), false), + ] + ); + }); + + // Each of the newly-loaded directories is scanned only once. + let read_dir_count_3 = fs.read_dir_call_count(); + assert_eq!(read_dir_count_3 - read_dir_count_2, 2); +} + +#[gpui::test(iterations = 10)] +async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/root", + json!({ + ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n", + "tree": { + ".git": {}, + ".gitignore": "ignored-dir\n", + "tracked-dir": { + "tracked-file1": "", + "ancestor-ignored-file1": "", + }, + "ignored-dir": { + "ignored-file1": "" + } + } + }), + ) + .await; + + let tree = Worktree::local( + build_client(cx), + "/root/tree".as_ref(), + true, + fs.clone(), Default::default(), &mut cx.to_async(), ) @@ -290,7 +754,15 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { .unwrap(); cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) .await; - tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + tree.as_local() + .unwrap() + .refresh_entries_for_paths(vec![Path::new("ignored-dir").into()]) + }) + .recv() + .await; + cx.read(|cx| { let tree = tree.read(cx); assert!( @@ -311,10 +783,26 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) { ); }); - std::fs::write(dir.join("tracked-dir/tracked-file2"), "").unwrap(); - std::fs::write(dir.join("tracked-dir/ancestor-ignored-file2"), "").unwrap(); - std::fs::write(dir.join("ignored-dir/ignored-file2"), "").unwrap(); - tree.flush_fs_events(cx).await; + fs.create_file( + "/root/tree/tracked-dir/tracked-file2".as_ref(), + Default::default(), + ) + .await + .unwrap(); + fs.create_file( + "/root/tree/tracked-dir/ancestor-ignored-file2".as_ref(), + Default::default(), + ) + .await + .unwrap(); + fs.create_file( + "/root/tree/ignored-dir/ignored-file2".as_ref(), + Default::default(), + ) + .await + .unwrap(); + + cx.foreground().run_until_parked(); cx.read(|cx| { let tree = tree.read(cx); assert!( @@ -346,10 +834,8 @@ async fn test_write_file(cx: &mut TestAppContext) { "ignored-dir": {} })); - let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - let tree = Worktree::local( - client, + build_client(cx), dir.path(), true, Arc::new(RealFs), @@ -393,8 +879,6 @@ async fn test_write_file(cx: &mut TestAppContext) { #[gpui::test(iterations = 30)] async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { - let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); - let fs = FakeFs::new(cx.background()); fs.insert_tree( "/root", @@ -407,7 +891,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { .await; let tree = Worktree::local( - client, + build_client(cx), "/root".as_ref(), true, fs, @@ -452,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, @@ -472,9 +1069,8 @@ async fn test_random_worktree_operations_during_initial_scan( } log::info!("generated initial tree"); - let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let worktree = Worktree::local( - client.clone(), + build_client(cx), root_dir, true, fs.clone(), @@ -506,7 +1102,7 @@ async fn test_random_worktree_operations_during_initial_scan( .await .log_err(); worktree.read_with(cx, |tree, _| { - tree.as_local().unwrap().snapshot().check_invariants() + tree.as_local().unwrap().snapshot().check_invariants(true) }); if rng.gen_bool(0.6) { @@ -523,7 +1119,7 @@ async fn test_random_worktree_operations_during_initial_scan( let final_snapshot = worktree.read_with(cx, |tree, _| { let tree = tree.as_local().unwrap(); let snapshot = tree.snapshot(); - snapshot.check_invariants(); + snapshot.check_invariants(true); snapshot }); @@ -562,9 +1158,8 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) } log::info!("generated initial tree"); - let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let worktree = Worktree::local( - client.clone(), + build_client(cx), root_dir, true, fs.clone(), @@ -627,12 +1222,17 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) log::info!("quiescing"); fs.as_fake().flush_events(usize::MAX); cx.foreground().run_until_parked(); + let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - snapshot.check_invariants(); + snapshot.check_invariants(true); + let expanded_paths = snapshot + .expanded_entries() + .map(|e| e.path.clone()) + .collect::>(); { let new_worktree = Worktree::local( - client.clone(), + build_client(cx), root_dir, true, fs.clone(), @@ -644,6 +1244,14 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) new_worktree .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) .await; + new_worktree + .update(cx, |tree, _| { + tree.as_local_mut() + .unwrap() + .refresh_entries_for_paths(expanded_paths) + }) + .recv() + .await; let new_snapshot = new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); assert_eq!( @@ -660,11 +1268,25 @@ async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) } assert_eq!( - prev_snapshot.entries(true).collect::>(), - snapshot.entries(true).collect::>(), + prev_snapshot + .entries(true) + .map(ignore_pending_dir) + .collect::>(), + snapshot + .entries(true) + .map(ignore_pending_dir) + .collect::>(), "wrong updates after snapshot {i}: {updates:#?}", ); } + + fn ignore_pending_dir(entry: &Entry) -> Entry { + let mut entry = entry.clone(); + if entry.kind.is_dir() { + entry.kind = EntryKind::Dir + } + entry + } } // The worktree's `UpdatedEntries` event can be used to follow along with @@ -679,7 +1301,6 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext ix, }; match change_type { - PathChange::Loaded => entries.insert(ix, entry.unwrap()), PathChange::Added => entries.insert(ix, entry.unwrap()), PathChange::Removed => drop(entries.remove(ix)), PathChange::Updated => { @@ -688,7 +1309,7 @@ fn check_worktree_change_events(tree: &mut Worktree, cx: &mut ModelContext { + PathChange::AddedOrUpdated | PathChange::Loaded => { let entry = entry.unwrap(); if entries.get(ix).map(|e| &e.path) == Some(&entry.path) { *entries.get_mut(ix).unwrap() = entry; @@ -947,10 +1568,8 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) { })); let root_path = root.path(); - let http_client = FakeHttpClient::with_404_response(); - let client = cx.read(|cx| Client::new(http_client, cx)); let tree = Worktree::local( - client, + build_client(cx), root_path, true, Arc::new(RealFs), @@ -1026,10 +1645,8 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) { }, })); - let http_client = FakeHttpClient::with_404_response(); - let client = cx.read(|cx| Client::new(http_client, cx)); let tree = Worktree::local( - client, + build_client(cx), root.path(), true, Arc::new(RealFs), @@ -1150,10 +1767,25 @@ async fn test_git_status(deterministic: Arc, cx: &mut TestAppCont })); - let http_client = FakeHttpClient::with_404_response(); - let client = cx.read(|cx| Client::new(http_client, cx)); + 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::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(A_TXT, &repo); + git_add(E_TXT, &repo); + git_add(DOTGITIGNORE, &repo); + git_commit("Initial commit", &repo); + let tree = Worktree::local( - client, + build_client(cx), root.path(), true, Arc::new(RealFs), @@ -1163,26 +1795,9 @@ async fn test_git_status(deterministic: Arc, cx: &mut TestAppCont .await .unwrap(); + tree.flush_fs_events(cx).await; 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 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_commit("Initial commit", &repo); - - tree.flush_fs_events(cx).await; deterministic.run_until_parked(); // Check that the right git state is observed on startup @@ -1202,39 +1817,39 @@ async fn test_git_status(deterministic: Arc, 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); @@ -1357,10 +1972,8 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { ], ); - let http_client = FakeHttpClient::with_404_response(); - let client = cx.read(|cx| Client::new(http_client, cx)); let tree = Worktree::local( - client, + build_client(cx), Path::new("/root"), true, fs.clone(), @@ -1439,6 +2052,11 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { } } +fn build_client(cx: &mut TestAppContext) -> Arc { + let http_client = FakeHttpClient::with_404_response(); + cx.read(|cx| Client::new(http_client, cx)) +} + #[track_caller] fn git_init(path: &Path) -> git2::Repository { git2::Repository::init(path).expect("Failed to initialize git repository") diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 55efc09deb..33606fccc4 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -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] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9563d54be8..c329ae4e51 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -64,7 +64,7 @@ pub struct ProjectPanel { pending_serialization: Task>, } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] struct Selection { worktree_id: WorktreeId, entry_id: ProjectEntryId, @@ -153,6 +153,7 @@ pub fn init(cx: &mut AppContext) { ); } +#[derive(Debug)] pub enum Event { OpenedEntry { entry_id: ProjectEntryId, @@ -410,17 +411,23 @@ impl ProjectPanel { fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { if let Some((worktree, entry)) = self.selected_entry(cx) { if entry.is_dir() { + let worktree_id = worktree.id(); + let entry_id = entry.id; let expanded_dir_ids = - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { expanded_dir_ids } else { return; }; - match expanded_dir_ids.binary_search(&entry.id) { + match expanded_dir_ids.binary_search(&entry_id) { Ok(_) => self.select_next(&SelectNext, cx), Err(ix) => { - expanded_dir_ids.insert(ix, entry.id); + self.project.update(cx, |project, cx| { + project.expand_entry(worktree_id, entry_id, cx); + }); + + expanded_dir_ids.insert(ix, entry_id); self.update_visible_entries(None, cx); cx.notify(); } @@ -431,18 +438,20 @@ impl ProjectPanel { fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { if let Some((worktree, mut entry)) = self.selected_entry(cx) { + let worktree_id = worktree.id(); let expanded_dir_ids = - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree.id()) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { expanded_dir_ids } else { return; }; loop { - match expanded_dir_ids.binary_search(&entry.id) { + let entry_id = entry.id; + match expanded_dir_ids.binary_search(&entry_id) { Ok(ix) => { expanded_dir_ids.remove(ix); - self.update_visible_entries(Some((worktree.id(), entry.id)), cx); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); cx.notify(); break; } @@ -463,14 +472,17 @@ impl ProjectPanel { fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); + self.project.update(cx, |project, cx| { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + project.expand_entry(worktree_id, entry_id, cx); + expanded_dir_ids.insert(ix, entry_id); + } } - Err(ix) => { - expanded_dir_ids.insert(ix, entry_id); - } - } + }); self.update_visible_entries(Some((worktree_id, entry_id)), cx); cx.focus_self(); cx.notify(); @@ -535,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; } @@ -576,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); @@ -938,10 +951,37 @@ impl ProjectPanel { } fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> { + let (worktree, entry) = self.selected_entry_handle(cx)?; + Some((worktree.read(cx), entry)) + } + + fn selected_entry_handle<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(ModelHandle, &'a project::Entry)> { let selection = self.selection?; let project = self.project.read(cx); - let worktree = project.worktree_for_id(selection.worktree_id, cx)?.read(cx); - Some((worktree, worktree.entry_for_id(selection.entry_id)?)) + let worktree = project.worktree_for_id(selection.worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(selection.entry_id)?; + Some((worktree, entry)) + } + + fn expand_to_selection(&mut self, cx: &mut ViewContext) -> 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( @@ -1002,6 +1042,7 @@ impl ProjectPanel { mtime: entry.mtime, is_symlink: false, is_ignored: false, + is_external: false, git_status: entry.git_status, }); } @@ -1058,29 +1099,31 @@ impl ProjectPanel { entry_id: ProjectEntryId, cx: &mut ViewContext, ) { - let project = self.project.read(cx); - if let Some((worktree, expanded_dir_ids)) = project - .worktree_for_id(worktree_id, cx) - .zip(self.expanded_dir_ids.get_mut(&worktree_id)) - { - let worktree = worktree.read(cx); + self.project.update(cx, |project, cx| { + if let Some((worktree, expanded_dir_ids)) = project + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + project.expand_entry(worktree_id, entry_id, cx); + let worktree = worktree.read(cx); - if let Some(mut entry) = worktree.entry_for_id(entry_id) { - loop { - if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { - expanded_dir_ids.insert(ix, entry.id); - } + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } - if let Some(parent_entry) = - entry.path.parent().and_then(|p| worktree.entry_for_path(p)) - { - entry = parent_entry; - } else { - break; + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } } } } - } + }); } fn for_each_visible_entry( @@ -1190,7 +1233,7 @@ impl ProjectPanel { Flex::row() .with_child( - if kind == EntryKind::Dir { + if kind.is_dir() { if details.is_expanded { Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color) } else { @@ -1253,7 +1296,10 @@ impl ProjectPanel { let show_editor = details.is_editing && !details.is_processing; MouseEventHandler::::new(entry_id.to_usize(), cx, |state, cx| { - let mut style = entry_style.style_for(state, details.is_selected).clone(); + let mut style = entry_style + .in_state(details.is_selected) + .style_for(state) + .clone(); if cx .global::>() @@ -1264,7 +1310,7 @@ impl ProjectPanel { .filter(|destination| details.path.starts_with(destination)) .is_some() { - style = entry_style.active.clone().unwrap(); + style = entry_style.active_state().default.clone(); } let row_container_style = if show_editor { @@ -1284,7 +1330,7 @@ impl ProjectPanel { }) .on_click(MouseButton::Left, move |event, this, cx| { if !show_editor { - if kind == EntryKind::Dir { + if kind.is_dir() { this.toggle_expanded(entry_id, cx); } else { this.open_entry(entry_id, event.click_count > 1, cx); @@ -1405,9 +1451,11 @@ impl View for ProjectPanel { let button_style = theme.open_project_button.clone(); let context_menu_item_style = theme::current(cx).context_menu.item.clone(); move |state, cx| { - let button_style = button_style.style_for(state, false).clone(); - let context_menu_item = - context_menu_item_style.style_for(state, true).clone(); + let button_style = button_style.style_for(state).clone(); + let context_menu_item = context_menu_item_style + .active_state() + .style_for(state) + .clone(); theme::ui::keystroke_label( "Open a project", @@ -1563,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; @@ -1973,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); @@ -2343,7 +2519,7 @@ mod tests { } let indent = " ".repeat(details.depth); - let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) { + let icon = if details.kind.is_dir() { if details.is_expanded { "v " } else { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 0dc5dad3bf..fc17b57c6d 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -196,7 +196,7 @@ impl PickerDelegate for ProjectSymbolsDelegate { ) -> AnyElement> { let theme = theme::current(cx); let style = &theme.picker.item; - let current_style = style.style_for(mouse_state, selected); + let current_style = style.in_state(selected).style_for(mouse_state); let string_match = &self.matches[ix]; let symbol = &self.symbols[string_match.candidate_id]; @@ -229,7 +229,10 @@ impl PickerDelegate for ProjectSymbolsDelegate { .with_child( // Avoid styling the path differently when it is selected, since // the symbol's syntax highlighting doesn't change when selected. - Label::new(path.to_string(), style.default.label.clone()), + Label::new( + path.to_string(), + style.inactive_state().default.label.clone(), + ), ) .contained() .with_style(current_style.container) diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 14f8853c9c..51774e8feb 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -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 diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index a1dc8982c7..4ba6103167 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -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; +pub fn build_recent_projects( + workspace: WeakViewHandle, + workspaces: Vec, + cx: &mut ViewContext, +) -> RecentProjects { + Picker::new( + RecentProjectsDelegate::new(workspace, workspaces, false), + cx, + ) + .with_theme(|theme| theme.picker.clone()) +} -struct RecentProjectsDelegate { +pub type RecentProjects = Picker; + +pub struct RecentProjectsDelegate { workspace: WeakViewHandle, workspace_locations: Vec, selected_match_index: usize, matches: Vec, + render_paths: bool, } impl RecentProjectsDelegate { fn new( workspace: WeakViewHandle, workspace_locations: Vec, + render_paths: bool, ) -> Self { Self { workspace, workspace_locations, selected_match_index: 0, matches: Default::default(), + render_paths, } } } @@ -173,7 +188,7 @@ impl PickerDelegate for RecentProjectsDelegate { cx: &gpui::AppContext, ) -> AnyElement> { let theme = theme::current(cx); - let style = theme.picker.item.style_for(mouse_state, selected); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); let string_match = &self.matches[ix]; @@ -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) diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 6b6f364fdb..2bfb090bb2 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -53,7 +53,7 @@ impl Rope { } } - self.chunks.push_tree(chunks.suffix(&()), &()); + self.chunks.append(chunks.suffix(&()), &()); self.check_invariants(); } @@ -384,6 +384,12 @@ impl<'a> From<&'a str> for Rope { } } +impl From for Rope { + fn from(text: String) -> Self { + Rope::from(text.as_str()) + } +} + impl fmt::Display for Rope { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for chunk in self.chunks() { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce4dd7f7cf..a0b98372b1 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -63,6 +63,8 @@ message Envelope { CopyProjectEntry copy_project_entry = 47; DeleteProjectEntry delete_project_entry = 48; ProjectEntryResponse project_entry_response = 49; + ExpandProjectEntry expand_project_entry = 114; + ExpandProjectEntryResponse expand_project_entry_response = 115; UpdateDiagnosticSummary update_diagnostic_summary = 50; StartLanguageServer start_language_server = 51; @@ -134,6 +136,10 @@ message Envelope { OnTypeFormattingResponse on_type_formatting_response = 112; UpdateWorktreeSettings update_worktree_settings = 113; + + InlayHints inlay_hints = 116; + InlayHintsResponse inlay_hints_response = 117; + RefreshInlayHints refresh_inlay_hints = 118; } } @@ -372,6 +378,15 @@ message DeleteProjectEntry { uint64 entry_id = 2; } +message ExpandProjectEntry { + uint64 project_id = 1; + uint64 entry_id = 2; +} + +message ExpandProjectEntryResponse { + uint64 worktree_scan_id = 1; +} + message ProjectEntryResponse { Entry entry = 1; uint64 worktree_scan_id = 2; @@ -694,6 +709,68 @@ message OnTypeFormattingResponse { Transaction transaction = 1; } +message InlayHints { + uint64 project_id = 1; + uint64 buffer_id = 2; + Anchor start = 3; + Anchor end = 4; + repeated VectorClockEntry version = 5; +} + +message InlayHintsResponse { + repeated InlayHint hints = 1; + repeated VectorClockEntry version = 2; +} + +message InlayHint { + Anchor position = 1; + InlayHintLabel label = 2; + optional string kind = 3; + bool padding_left = 4; + bool padding_right = 5; + InlayHintTooltip tooltip = 6; +} + +message InlayHintLabel { + oneof label { + string value = 1; + InlayHintLabelParts label_parts = 2; + } +} + +message InlayHintLabelParts { + repeated InlayHintLabelPart parts = 1; +} + +message InlayHintLabelPart { + string value = 1; + InlayHintLabelPartTooltip tooltip = 2; + Location location = 3; +} + +message InlayHintTooltip { + oneof content { + string value = 1; + MarkupContent markup_content = 2; + } +} + +message InlayHintLabelPartTooltip { + oneof content { + string value = 1; + MarkupContent markup_content = 2; + } +} + +message RefreshInlayHints { + uint64 project_id = 1; +} + +message MarkupContent { + string kind = 1; + string value = 2; +} + message PerformRenameResponse { ProjectTransaction transaction = 2; } @@ -1005,7 +1082,8 @@ message Entry { Timestamp mtime = 5; bool is_symlink = 6; bool is_ignored = 7; - optional GitStatus git_status = 8; + bool is_external = 8; + optional GitStatus git_status = 9; } message RepositoryEntry { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 13794ea64d..605b05a562 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -150,6 +150,7 @@ messages!( (DeclineCall, Foreground), (DeleteProjectEntry, Foreground), (Error, Foreground), + (ExpandProjectEntry, Foreground), (Follow, Foreground), (FollowResponse, Foreground), (FormatBuffers, Foreground), @@ -197,9 +198,13 @@ messages!( (PerformRenameResponse, Background), (OnTypeFormatting, Background), (OnTypeFormattingResponse, Background), + (InlayHints, Background), + (InlayHintsResponse, Background), + (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), (PrepareRenameResponse, Background), + (ExpandProjectEntryResponse, Foreground), (ProjectEntryResponse, Foreground), (RejoinRoom, Foreground), (RejoinRoomResponse, Foreground), @@ -255,6 +260,7 @@ request_messages!( (CreateRoom, CreateRoomResponse), (DeclineCall, Ack), (DeleteProjectEntry, ProjectEntryResponse), + (ExpandProjectEntry, ExpandProjectEntryResponse), (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), @@ -283,6 +289,8 @@ request_messages!( (PerformRename, PerformRenameResponse), (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), + (InlayHints, InlayHintsResponse), + (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), (RemoveContact, Ack), @@ -311,6 +319,7 @@ entity_messages!( CreateBufferForPeer, CreateProjectEntry, DeleteProjectEntry, + ExpandProjectEntry, Follow, FormatBuffers, GetCodeActions, @@ -328,6 +337,8 @@ entity_messages!( OpenBufferForSymbol, PerformRename, OnTypeFormatting, + InlayHints, + RefreshInlayHints, PrepareRename, ReloadBuffers, RemoveProjectCollaborator, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 8b10167091..6b430d90e4 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 58; +pub const PROTOCOL_VERSION: u32 = 59; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 87a8b265fb..59d25c2659 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -259,7 +259,11 @@ impl BufferSearchBar { } } - fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + pub fn is_dismissed(&self) -> bool { + self.dismissed + } + + pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { self.dismissed = true; for searchable_item in self.seachable_items_with_matches.keys() { if let Some(searchable_item) = @@ -275,7 +279,7 @@ impl BufferSearchBar { cx.notify(); } - fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext) -> bool { + pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext) -> bool { let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { @@ -328,7 +332,11 @@ impl BufferSearchBar { Some( MouseEventHandler::::new(option as usize, cx, |state, cx| { let theme = theme::current(cx); - let style = theme.search.option_button.style_for(state, is_active); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) @@ -371,7 +379,7 @@ impl BufferSearchBar { enum NavButton {} MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); - let style = theme.search.option_button.style_for(state, false); + let style = theme.search.option_button.inactive_state().style_for(state); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) @@ -403,7 +411,7 @@ impl BufferSearchBar { enum CloseButton {} MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state, false); + let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) .constrained() @@ -480,7 +488,7 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } - fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { + pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 27aac1762b..135194df6a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -896,7 +896,7 @@ impl ProjectSearchBar { enum NavButton {} MouseEventHandler::::new(direction as usize, cx, |state, cx| { let theme = theme::current(cx); - let style = theme.search.option_button.style_for(state, false); + let style = theme.search.option_button.inactive_state().style_for(state); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) @@ -927,7 +927,11 @@ impl ProjectSearchBar { let is_active = self.is_option_enabled(option, cx); MouseEventHandler::::new(option as usize, cx, |state, cx| { let theme = theme::current(cx); - let style = theme.search.option_button.style_for(state, is_active); + let style = theme + .search + .option_button + .in_state(is_active) + .style_for(state); Label::new(icon, style.text.clone()) .contained() .with_style(style.container) diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index dab4b91992..06b81a0c61 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -21,7 +21,7 @@ util = { path = "../util" } anyhow.workspace = true futures.workspace = true -json_comments = "0.2" +serde_json_lenient = {version = "0.1", features = ["preserve_order", "raw_value"]} lazy_static.workspace = true postage.workspace = true rust-embed.workspace = true @@ -37,6 +37,6 @@ tree-sitter-json = "*" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } - -pretty_assertions = "1.3.0" +indoc.workspace = true +pretty_assertions.workspace = true unindent.workspace = true diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index e607a254bd..93cb2ab3d7 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,30 +1,30 @@ use crate::{settings_store::parse_json_with_comments, SettingsAssets}; -use anyhow::{Context, Result}; +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}, JsonSchema, }; use serde::Deserialize; -use serde_json::{value::RawValue, Value}; +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); -#[derive(Deserialize, Default, Clone, JsonSchema)] +#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] pub struct KeymapBlock { #[serde(default)] context: Option, bindings: BTreeMap, } -#[derive(Deserialize, Default, Clone)] +#[derive(Debug, Deserialize, Default, Clone)] #[serde(transparent)] -pub struct KeymapAction(Box); +pub struct KeymapAction(Value); impl JsonSchema for KeymapAction { fn schema_name() -> String { @@ -37,11 +37,12 @@ impl JsonSchema for KeymapAction { } #[derive(Deserialize)] -struct ActionWithData(Box, Box); +struct ActionWithData(Box, Value); impl KeymapFile { pub fn load_asset(asset_path: &str, cx: &mut AppContext) -> Result<()> { let content = asset_str::(asset_path); + Self::parse(content.as_ref())?.add_to_cx(cx) } @@ -54,18 +55,28 @@ impl KeymapFile { let bindings = bindings .into_iter() .filter_map(|(keystroke, action)| { - let action = action.0.get(); + let action = action.0; // This is a workaround for a limitation in serde: serde-rs/json#497 // 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 action.starts_with('[') { - let ActionWithData(name, data) = serde_json::from_str(action).log_err()?; - cx.deserialize_action(&name, Some(data.get())) - } else { - let name = serde_json::from_str(action).log_err()?; - cx.deserialize_action(name, None) + 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!( @@ -105,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() })), @@ -118,3 +133,28 @@ impl KeymapFile { serde_json::to_value(root_schema).unwrap() } } + +fn no_action() -> Box { + Box::new(NoAction {}) +} + +#[cfg(test)] +mod tests { + use crate::KeymapFile; + + #[test] + fn can_deserialize_keymap_with_trailing_comma() { + let json = indoc::indoc! {"[ + // Standard macOS bindings + { + \"bindings\": { + \"up\": \"menu::SelectPrev\", + }, + }, + ] + " + + }; + KeymapFile::parse(json).unwrap(); + } +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 39c6a2c122..1188018cd8 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -834,11 +834,8 @@ fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: } pub fn parse_json_with_comments(content: &str) -> Result { - Ok(serde_json::from_reader( - json_comments::CommentSettings::c_style().strip_comments(content.as_bytes()), - )?) + Ok(serde_json_lenient::from_str(content)?) } - #[cfg(test)] mod tests { use super::*; diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 88412f6059..59165283f6 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -97,6 +97,42 @@ where } } + pub fn next_item(&self) -> Option<&'a T> { + self.assert_did_seek(); + if let Some(entry) = self.stack.last() { + if entry.index == entry.tree.0.items().len() - 1 { + if let Some(next_leaf) = self.next_leaf() { + Some(next_leaf.0.items().first().unwrap()) + } else { + None + } + } else { + match *entry.tree.0 { + Node::Leaf { ref items, .. } => Some(&items[entry.index + 1]), + _ => unreachable!(), + } + } + } else if self.at_end { + None + } else { + self.tree.first() + } + } + + fn next_leaf(&self) -> Option<&'a SumTree> { + for entry in self.stack.iter().rev().skip(1) { + if entry.index < entry.tree.0.child_trees().len() - 1 { + match *entry.tree.0 { + Node::Internal { + ref child_trees, .. + } => return Some(child_trees[entry.index + 1].leftmost_leaf()), + Node::Leaf { .. } => unreachable!(), + }; + } + } + None + } + pub fn prev_item(&self) -> Option<&'a T> { self.assert_did_seek(); if let Some(entry) = self.stack.last() { @@ -669,7 +705,7 @@ impl<'a, T: Item> SeekAggregate<'a, T> for () { impl<'a, T: Item> SeekAggregate<'a, T> for SliceSeekAggregate { fn begin_leaf(&mut self) {} fn end_leaf(&mut self, cx: &::Context) { - self.tree.push_tree( + self.tree.append( SumTree(Arc::new(Node::Leaf { summary: mem::take(&mut self.leaf_summary), items: mem::take(&mut self.leaf_items), @@ -689,7 +725,7 @@ impl<'a, T: Item> SeekAggregate<'a, T> for SliceSeekAggregate { _: &T::Summary, cx: &::Context, ) { - self.tree.push_tree(tree.clone(), cx); + self.tree.append(tree.clone(), cx); } } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 6c6150aa3a..8d219ca021 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -95,31 +95,18 @@ impl fmt::Debug for End { } } -#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)] pub enum Bias { + #[default] Left, Right, } -impl Default for Bias { - fn default() -> Self { - Bias::Left - } -} - -impl PartialOrd for Bias { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Bias { - fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - (Self::Left, Self::Left) => Ordering::Equal, - (Self::Left, Self::Right) => Ordering::Less, - (Self::Right, Self::Right) => Ordering::Equal, - (Self::Right, Self::Left) => Ordering::Greater, +impl Bias { + pub fn invert(self) -> Self { + match self { + Self::Left => Self::Right, + Self::Right => Self::Left, } } } @@ -268,7 +255,7 @@ impl SumTree { for item in iter { if leaf.is_some() && leaf.as_ref().unwrap().items().len() == 2 * TREE_BASE { - self.push_tree(SumTree(Arc::new(leaf.take().unwrap())), cx); + self.append(SumTree(Arc::new(leaf.take().unwrap())), cx); } if leaf.is_none() { @@ -295,13 +282,13 @@ impl SumTree { } if leaf.is_some() { - self.push_tree(SumTree(Arc::new(leaf.take().unwrap())), cx); + self.append(SumTree(Arc::new(leaf.take().unwrap())), cx); } } pub fn push(&mut self, item: T, cx: &::Context) { let summary = item.summary(); - self.push_tree( + self.append( SumTree(Arc::new(Node::Leaf { summary: summary.clone(), items: ArrayVec::from_iter(Some(item)), @@ -311,11 +298,11 @@ impl SumTree { ); } - pub fn push_tree(&mut self, other: Self, cx: &::Context) { + pub fn append(&mut self, other: Self, cx: &::Context) { if !other.0.is_leaf() || !other.0.items().is_empty() { if self.0.height() < other.0.height() { for tree in other.0.child_trees() { - self.push_tree(tree.clone(), cx); + self.append(tree.clone(), cx); } } else if let Some(split_tree) = self.push_tree_recursive(other, cx) { *self = Self::from_child_trees(self.clone(), split_tree, cx); @@ -512,7 +499,7 @@ impl SumTree { } } new_tree.push(item, cx); - new_tree.push_tree(cursor.suffix(cx), cx); + new_tree.append(cursor.suffix(cx), cx); new_tree }; replaced @@ -529,7 +516,7 @@ impl SumTree { cursor.next(cx); } } - new_tree.push_tree(cursor.suffix(cx), cx); + new_tree.append(cursor.suffix(cx), cx); new_tree }; removed @@ -563,7 +550,7 @@ impl SumTree { { new_tree.extend(buffered_items.drain(..), cx); let slice = cursor.slice(&new_key, Bias::Left, cx); - new_tree.push_tree(slice, cx); + new_tree.append(slice, cx); old_item = cursor.item(); } @@ -583,7 +570,7 @@ impl SumTree { } new_tree.extend(buffered_items, cx); - new_tree.push_tree(cursor.suffix(cx), cx); + new_tree.append(cursor.suffix(cx), cx); new_tree }; @@ -719,7 +706,7 @@ mod tests { let mut tree2 = SumTree::new(); tree2.extend(50..100, &()); - tree1.push_tree(tree2, &()); + tree1.append(tree2, &()); assert_eq!( tree1.items(&()), (0..20).chain(50..100).collect::>() @@ -766,7 +753,7 @@ mod tests { let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right, &()); new_tree.extend(new_items, &()); cursor.seek(&Count(splice_end), Bias::Right, &()); - new_tree.push_tree(cursor.slice(&tree_end, Bias::Right, &()), &()); + new_tree.append(cursor.slice(&tree_end, Bias::Right, &()), &()); new_tree }; @@ -838,6 +825,14 @@ mod tests { assert_eq!(cursor.item(), None); } + if before_start { + assert_eq!(cursor.next_item(), reference_items.get(0)); + } else if pos + 1 < reference_items.len() { + assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]); + } else { + assert_eq!(cursor.next_item(), None); + } + if i < 5 { cursor.next(&()); if pos < reference_items.len() { @@ -883,14 +878,17 @@ mod tests { ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); cursor.prev(&()); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); cursor.next(&()); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); // Single-element tree @@ -903,22 +901,26 @@ mod tests { ); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); cursor.next(&()); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 1); cursor.prev(&()); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); let mut cursor = tree.cursor::(); assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 1); cursor.seek(&Count(0), Bias::Right, &()); @@ -930,6 +932,7 @@ mod tests { ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 1); // Multiple-element tree @@ -940,67 +943,80 @@ mod tests { assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]); assert_eq!(cursor.item(), Some(&3)); assert_eq!(cursor.prev_item(), Some(&2)); + assert_eq!(cursor.next_item(), Some(&4)); assert_eq!(cursor.start().sum, 3); cursor.next(&()); assert_eq!(cursor.item(), Some(&4)); assert_eq!(cursor.prev_item(), Some(&3)); + assert_eq!(cursor.next_item(), Some(&5)); assert_eq!(cursor.start().sum, 6); cursor.next(&()); assert_eq!(cursor.item(), Some(&5)); assert_eq!(cursor.prev_item(), Some(&4)); + assert_eq!(cursor.next_item(), Some(&6)); assert_eq!(cursor.start().sum, 10); cursor.next(&()); assert_eq!(cursor.item(), Some(&6)); assert_eq!(cursor.prev_item(), Some(&5)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 15); cursor.next(&()); cursor.next(&()); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 21); cursor.prev(&()); assert_eq!(cursor.item(), Some(&6)); assert_eq!(cursor.prev_item(), Some(&5)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 15); cursor.prev(&()); assert_eq!(cursor.item(), Some(&5)); assert_eq!(cursor.prev_item(), Some(&4)); + assert_eq!(cursor.next_item(), Some(&6)); assert_eq!(cursor.start().sum, 10); cursor.prev(&()); assert_eq!(cursor.item(), Some(&4)); assert_eq!(cursor.prev_item(), Some(&3)); + assert_eq!(cursor.next_item(), Some(&5)); assert_eq!(cursor.start().sum, 6); cursor.prev(&()); assert_eq!(cursor.item(), Some(&3)); assert_eq!(cursor.prev_item(), Some(&2)); + assert_eq!(cursor.next_item(), Some(&4)); assert_eq!(cursor.start().sum, 3); cursor.prev(&()); assert_eq!(cursor.item(), Some(&2)); assert_eq!(cursor.prev_item(), Some(&1)); + assert_eq!(cursor.next_item(), Some(&3)); assert_eq!(cursor.start().sum, 1); cursor.prev(&()); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&2)); assert_eq!(cursor.start().sum, 0); cursor.prev(&()); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&1)); assert_eq!(cursor.start().sum, 0); cursor.next(&()); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.next_item(), Some(&2)); assert_eq!(cursor.start().sum, 0); let mut cursor = tree.cursor::(); @@ -1012,6 +1028,7 @@ mod tests { ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 21); cursor.seek(&Count(3), Bias::Right, &()); @@ -1023,6 +1040,7 @@ mod tests { ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&6)); + assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 21); // Seeking can bias left or right diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index ea69fb0dca..4bb98d2ac8 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -67,7 +67,7 @@ impl TreeMap { removed = Some(cursor.item().unwrap().value.clone()); cursor.next(&()); } - new_tree.push_tree(cursor.suffix(&()), &()); + new_tree.append(cursor.suffix(&()), &()); drop(cursor); self.0 = new_tree; removed @@ -79,7 +79,7 @@ impl TreeMap { let mut cursor = self.0.cursor::>(); let mut new_tree = cursor.slice(&start, Bias::Left, &()); cursor.seek(&end, Bias::Left, &()); - new_tree.push_tree(cursor.suffix(&()), &()); + new_tree.append(cursor.suffix(&()), &()); drop(cursor); self.0 = new_tree; } @@ -117,7 +117,7 @@ impl TreeMap { new_tree.push(updated, &()); cursor.next(&()); } - new_tree.push_tree(cursor.suffix(&()), &()); + new_tree.append(cursor.suffix(&()), &()); drop(cursor); self.0 = new_tree; result diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 2f2ff2cdc3..b92059f5d6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -395,16 +395,17 @@ impl TerminalElement { // Terminal Emulator controlled behavior: region = region // Start selections - .on_down( - MouseButton::Left, - TerminalElement::generic_button_handler( - connection, - origin, - move |terminal, origin, e, _cx| { - terminal.mouse_down(&e, origin); - }, - ), - ) + .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { + cx.focus_parent(); + v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); + if let Some(conn_handle) = connection.upgrade(cx) { + conn_handle.update(cx, |terminal, cx| { + terminal.mouse_down(&event, origin); + + cx.notify(); + }) + } + }) // Update drag selections .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { if cx.is_self_focused() { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ac3875af9e..11f8f7abde 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -25,6 +25,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(TerminalPanel::new_terminal); } +#[derive(Debug)] pub enum Event { Close, DockPositionChanged, @@ -86,6 +87,7 @@ impl TerminalPanel { } }) }, + |_, _| {}, None, )) .with_child(Pane::render_tab_bar_button( @@ -99,6 +101,7 @@ impl TerminalPanel { Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))), cx, move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + |_, _| {}, None, )) .into_any() diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 2693add8ed..7c94f25e1e 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -600,7 +600,7 @@ impl Buffer { let mut old_fragments = self.fragments.cursor::(); let mut new_fragments = old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right, &None); - new_ropes.push_tree(new_fragments.summary().text); + new_ropes.append(new_fragments.summary().text); let mut fragment_start = old_fragments.start().visible; for (range, new_text) in edits { @@ -625,8 +625,8 @@ impl Buffer { } let slice = old_fragments.slice(&range.start, Bias::Right, &None); - new_ropes.push_tree(slice.summary().text); - new_fragments.push_tree(slice, &None); + new_ropes.append(slice.summary().text); + new_fragments.append(slice, &None); fragment_start = old_fragments.start().visible; } @@ -728,8 +728,8 @@ impl Buffer { } let suffix = old_fragments.suffix(&None); - new_ropes.push_tree(suffix.summary().text); - new_fragments.push_tree(suffix, &None); + new_ropes.append(suffix.summary().text); + new_fragments.append(suffix, &None); let (visible_text, deleted_text) = new_ropes.finish(); drop(old_fragments); @@ -828,7 +828,7 @@ impl Buffer { Bias::Left, &cx, ); - new_ropes.push_tree(new_fragments.summary().text); + new_ropes.append(new_fragments.summary().text); let mut fragment_start = old_fragments.start().0.full_offset(); for (range, new_text) in edits { @@ -854,8 +854,8 @@ impl Buffer { let slice = old_fragments.slice(&VersionedFullOffset::Offset(range.start), Bias::Left, &cx); - new_ropes.push_tree(slice.summary().text); - new_fragments.push_tree(slice, &None); + new_ropes.append(slice.summary().text); + new_fragments.append(slice, &None); fragment_start = old_fragments.start().0.full_offset(); } @@ -986,8 +986,8 @@ impl Buffer { } let suffix = old_fragments.suffix(&cx); - new_ropes.push_tree(suffix.summary().text); - new_fragments.push_tree(suffix, &None); + new_ropes.append(suffix.summary().text); + new_fragments.append(suffix, &None); let (visible_text, deleted_text) = new_ropes.finish(); drop(old_fragments); @@ -1056,8 +1056,8 @@ impl Buffer { for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) { let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None); - new_ropes.push_tree(preceding_fragments.summary().text); - new_fragments.push_tree(preceding_fragments, &None); + new_ropes.append(preceding_fragments.summary().text); + new_fragments.append(preceding_fragments, &None); if let Some(fragment) = old_fragments.item() { let mut fragment = fragment.clone(); @@ -1087,8 +1087,8 @@ impl Buffer { } let suffix = old_fragments.suffix(&None); - new_ropes.push_tree(suffix.summary().text); - new_fragments.push_tree(suffix, &None); + new_ropes.append(suffix.summary().text); + new_fragments.append(suffix, &None); drop(old_fragments); let (visible_text, deleted_text) = new_ropes.finish(); @@ -2070,7 +2070,7 @@ impl<'a> RopeBuilder<'a> { } } - fn push_tree(&mut self, len: FragmentTextSummary) { + fn append(&mut self, len: FragmentTextSummary) { self.push(len.visible, true, true); self.push(len.deleted, false, false); } @@ -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 } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 95d6348c62..1949a5d9bb 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -4,15 +4,16 @@ pub mod ui; use gpui::{ color::Color, - elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, TooltipStyle}, + elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, fonts::{HighlightStyle, TextStyle}, platform, AppContext, AssetSource, Border, MouseState, }; +use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use settings::SettingsStore; use std::{collections::HashMap, sync::Arc}; -use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle, SvgStyle}; +use ui::{ButtonStyle, CheckboxStyle, IconStyle, ModalStyle}; pub use theme_registry::*; pub use theme_settings::*; @@ -36,7 +37,7 @@ pub fn init(source: impl AssetSource, cx: &mut AppContext) { .detach(); } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct Theme { #[serde(default)] pub meta: ThemeMeta, @@ -64,10 +65,10 @@ pub struct Theme { pub assistant: AssistantStyle, pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, - pub color_scheme: ColorScheme, + pub titlebar: Titlebar, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, JsonSchema)] pub struct ThemeMeta { #[serde(skip_deserializing)] pub id: usize, @@ -75,11 +76,10 @@ pub struct ThemeMeta { pub is_light: bool, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct Workspace { pub background: Color, pub blank_pane: BlankPaneStyle, - pub titlebar: Titlebar, pub tab_bar: TabBar, pub pane_divider: Border, pub leader_border_opacity: f32, @@ -102,7 +102,7 @@ pub struct Workspace { pub drop_target_overlay_color: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct BlankPaneStyle { pub logo: SvgStyle, pub logo_shadow: SvgStyle, @@ -112,13 +112,14 @@ pub struct BlankPaneStyle { pub keyboard_hint_width: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Titlebar { #[serde(flatten)] pub container: ContainerStyle, pub height: f32, - pub title: TextStyle, - pub highlight_color: Color, + pub project_menu_button: Toggleable>, + pub project_name_divider: ContainedText, + pub git_menu_button: Toggleable>, pub item_spacing: f32, pub face_pile_spacing: f32, pub avatar_ribbon: AvatarRibbon, @@ -128,16 +129,34 @@ pub struct Titlebar { pub leader_avatar: AvatarStyle, pub follower_avatar: AvatarStyle, pub inactive_avatar_grayscale: bool, - pub sign_in_prompt: Interactive, + pub sign_in_button: Toggleable>, pub outdated_warning: ContainedText, - pub share_button: Interactive, - pub call_control: Interactive, - pub toggle_contacts_button: Interactive, - pub user_menu_button: Interactive, + pub share_button: Toggleable>, + pub muted: Color, + pub speaking: Color, + pub screen_share_button: Toggleable>, + pub toggle_contacts_button: Toggleable>, + pub toggle_microphone_button: Toggleable>, + pub toggle_speakers_button: Toggleable>, + pub leave_call_button: Interactive, pub toggle_contacts_badge: ContainerStyle, + pub user_menu: UserMenu, } -#[derive(Copy, Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct UserMenu { + pub user_menu_button_online: UserMenuButton, + pub user_menu_button_offline: UserMenuButton, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct UserMenuButton { + pub user_menu: Toggleable>, + pub avatar: AvatarStyle, + pub icon: Icon, +} + +#[derive(Copy, Clone, Deserialize, Default, JsonSchema)] pub struct AvatarStyle { #[serde(flatten)] pub image: ImageStyle, @@ -145,14 +164,14 @@ pub struct AvatarStyle { pub outer_corner_radius: f32, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, JsonSchema)] pub struct Copilot { pub out_link_icon: Interactive, pub modal: ModalStyle, pub auth: CopilotAuth, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, JsonSchema)] pub struct CopilotAuth { pub content_width: f32, pub prompting: CopilotAuthPrompting, @@ -162,14 +181,14 @@ pub struct CopilotAuth { pub header: IconStyle, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, JsonSchema)] pub struct CopilotAuthPrompting { pub subheading: ContainedText, pub hint: ContainedText, pub device_code: DeviceCode, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, JsonSchema)] pub struct DeviceCode { pub text: TextStyle, pub cta: ButtonStyle, @@ -179,19 +198,19 @@ pub struct DeviceCode { pub right_container: Interactive, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, JsonSchema)] pub struct CopilotAuthNotAuthorized { pub subheading: ContainedText, pub warning: ContainedText, } -#[derive(Deserialize, Default, Clone)] +#[derive(Deserialize, Default, Clone, JsonSchema)] pub struct CopilotAuthAuthorized { pub subheading: ContainedText, pub hint: ContainedText, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ContactsPopover { #[serde(flatten)] pub container: ContainerStyle, @@ -199,17 +218,17 @@ pub struct ContactsPopover { pub width: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ContactList { pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, pub add_contact_button: IconButton, - pub header_row: Interactive, + pub header_row: Toggleable>, pub leave_call: Interactive, - pub contact_row: Interactive, + pub contact_row: Toggleable>, pub row_height: f32, - pub project_row: Interactive, - pub tree_branch: Interactive, + pub project_row: Toggleable>, + pub tree_branch: Toggleable>, pub contact_avatar: ImageStyle, pub contact_status_free: ContainerStyle, pub contact_status_busy: ContainerStyle, @@ -221,7 +240,7 @@ pub struct ContactList { pub calling_indicator: ContainedText, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ProjectRow { #[serde(flatten)] pub container: ContainerStyle, @@ -229,13 +248,13 @@ pub struct ProjectRow { pub name: ContainedText, } -#[derive(Deserialize, Default, Clone, Copy)] +#[derive(Deserialize, Default, Clone, Copy, JsonSchema)] pub struct TreeBranch { pub width: f32, pub color: Color, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ContactFinder { pub picker: Picker, pub row_height: f32, @@ -245,17 +264,17 @@ pub struct ContactFinder { pub disabled_contact_button: IconButton, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct DropdownMenu { #[serde(flatten)] pub container: ContainerStyle, pub header: Interactive, pub section_header: ContainedText, - pub item: Interactive, + pub item: Toggleable>, pub row_height: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct DropdownMenuItem { #[serde(flatten)] pub container: ContainerStyle, @@ -266,11 +285,11 @@ pub struct DropdownMenuItem { pub secondary_text_spacing: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct TabBar { #[serde(flatten)] pub container: ContainerStyle, - pub pane_button: Interactive, + pub pane_button: Toggleable>, pub pane_button_container: ContainerStyle, pub active_pane: TabStyles, pub inactive_pane: TabStyles, @@ -294,13 +313,13 @@ impl TabBar { } } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct TabStyles { pub active_tab: Tab, pub inactive_tab: Tab, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct AvatarRibbon { #[serde(flatten)] pub container: ContainerStyle, @@ -308,7 +327,7 @@ pub struct AvatarRibbon { pub height: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct OfflineIcon { #[serde(flatten)] pub container: ContainerStyle, @@ -316,7 +335,7 @@ pub struct OfflineIcon { pub color: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Tab { pub height: f32, #[serde(flatten)] @@ -333,7 +352,7 @@ pub struct Tab { pub icon_conflict: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Toolbar { #[serde(flatten)] pub container: ContainerStyle, @@ -342,14 +361,14 @@ pub struct Toolbar { pub nav_button: Interactive, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Notifications { #[serde(flatten)] pub container: ContainerStyle, pub width: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Search { #[serde(flatten)] pub container: ContainerStyle, @@ -359,14 +378,14 @@ pub struct Search { pub include_exclude_editor: FindEditor, pub invalid_include_exclude_editor: ContainerStyle, pub include_exclude_inputs: ContainedText, - pub option_button: Interactive, + pub option_button: Toggleable>, pub match_background: Color, pub match_index: ContainedText, pub results_status: TextStyle, pub dismiss_button: Interactive, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct FindEditor { #[serde(flatten)] pub input: FieldEditor, @@ -374,7 +393,7 @@ pub struct FindEditor { pub max_width: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct StatusBar { #[serde(flatten)] pub container: ContainerStyle, @@ -390,15 +409,15 @@ pub struct StatusBar { pub diagnostic_message: Interactive, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct StatusBarPanelButtons { pub group_left: ContainerStyle, pub group_bottom: ContainerStyle, pub group_right: ContainerStyle, - pub button: Interactive, + pub button: Toggleable>, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct StatusBarDiagnosticSummary { pub container_ok: ContainerStyle, pub container_warning: ContainerStyle, @@ -413,7 +432,7 @@ pub struct StatusBarDiagnosticSummary { pub summary_spacing: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct StatusBarLspStatus { #[serde(flatten)] pub container: ContainerStyle, @@ -424,14 +443,14 @@ pub struct StatusBarLspStatus { pub message: TextStyle, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct Dock { pub left: ContainerStyle, pub bottom: ContainerStyle, pub right: ContainerStyle, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct PanelButton { #[serde(flatten)] pub container: ContainerStyle, @@ -440,20 +459,20 @@ pub struct PanelButton { pub label: ContainedText, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ProjectPanel { #[serde(flatten)] pub container: ContainerStyle, - pub entry: Interactive, + pub entry: Toggleable>, pub dragged_entry: ProjectPanelEntry, - pub ignored_entry: Interactive, - pub cut_entry: Interactive, + pub ignored_entry: Toggleable>, + pub cut_entry: Toggleable>, pub filename_editor: FieldEditor, pub indent_width: f32, pub open_project_button: Interactive, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct ProjectPanelEntry { pub height: f32, #[serde(flatten)] @@ -465,28 +484,28 @@ pub struct ProjectPanelEntry { pub status: EntryStatus, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct EntryStatus { pub git: GitProjectStatus, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct GitProjectStatus { pub modified: Color, pub inserted: Color, pub conflict: Color, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct ContextMenu { #[serde(flatten)] pub container: ContainerStyle, - pub item: Interactive, + pub item: Toggleable>, pub keystroke_margin: f32, pub separator: ContainerStyle, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct ContextMenuItem { #[serde(flatten)] pub container: ContainerStyle, @@ -496,13 +515,13 @@ pub struct ContextMenuItem { pub icon_spacing: f32, } -#[derive(Debug, Deserialize, Default)] +#[derive(Debug, Deserialize, Default, JsonSchema)] pub struct CommandPalette { - pub key: Interactive, + pub key: Toggleable, pub keystroke_spacing: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct InviteLink { #[serde(flatten)] pub container: ContainerStyle, @@ -511,7 +530,7 @@ pub struct InviteLink { pub icon: Icon, } -#[derive(Deserialize, Clone, Copy, Default)] +#[derive(Deserialize, Clone, Copy, Default, JsonSchema)] pub struct Icon { #[serde(flatten)] pub container: ContainerStyle, @@ -519,7 +538,7 @@ pub struct Icon { pub width: f32, } -#[derive(Deserialize, Clone, Copy, Default)] +#[derive(Deserialize, Clone, Copy, Default, JsonSchema)] pub struct IconButton { #[serde(flatten)] pub container: ContainerStyle, @@ -528,7 +547,7 @@ pub struct IconButton { pub button_width: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ChatMessage { #[serde(flatten)] pub container: ContainerStyle, @@ -537,7 +556,7 @@ pub struct ChatMessage { pub timestamp: ContainedText, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ChannelSelect { #[serde(flatten)] pub container: ContainerStyle, @@ -549,7 +568,7 @@ pub struct ChannelSelect { pub menu: ContainerStyle, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ChannelName { #[serde(flatten)] pub container: ContainerStyle, @@ -557,7 +576,7 @@ pub struct ChannelName { pub name: TextStyle, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Picker { #[serde(flatten)] pub container: ContainerStyle, @@ -565,10 +584,12 @@ pub struct Picker { pub input_editor: FieldEditor, pub empty_input_editor: FieldEditor, pub no_matches: ContainedLabel, - pub item: Interactive, + pub item: Toggleable>, + pub header: ContainedLabel, + pub footer: ContainedLabel, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct ContainedText { #[serde(flatten)] pub container: ContainerStyle, @@ -576,7 +597,7 @@ pub struct ContainedText { pub text: TextStyle, } -#[derive(Clone, Debug, Deserialize, Default)] +#[derive(Clone, Debug, Deserialize, Default, JsonSchema)] pub struct ContainedLabel { #[serde(flatten)] pub container: ContainerStyle, @@ -584,7 +605,7 @@ pub struct ContainedLabel { pub label: LabelStyle, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct ProjectDiagnostics { #[serde(flatten)] pub container: ContainerStyle, @@ -594,7 +615,7 @@ pub struct ProjectDiagnostics { pub tab_summary_spacing: f32, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ContactNotification { pub header_avatar: ImageStyle, pub header_message: ContainedText, @@ -604,21 +625,21 @@ pub struct ContactNotification { pub dismiss_button: Interactive, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct UpdateNotification { pub message: ContainedText, pub action_message: Interactive, pub dismiss_button: Interactive, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct MessageNotification { pub message: ContainedText, pub action_message: Interactive, pub dismiss_button: Interactive, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct ProjectSharedNotification { pub window_height: f32, pub window_width: f32, @@ -635,7 +656,7 @@ pub struct ProjectSharedNotification { pub dismiss_button: ContainedText, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, JsonSchema)] pub struct IncomingCallNotification { pub window_height: f32, pub window_width: f32, @@ -652,7 +673,7 @@ pub struct IncomingCallNotification { pub decline_button: ContainedText, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Editor { pub text_color: Color, #[serde(default)] @@ -670,6 +691,7 @@ pub struct Editor { pub line_number_active: Color, pub guest_selections: Vec, pub syntax: Arc, + pub hint: HighlightStyle, pub suggestion: HighlightStyle, pub diagnostic_path_header: DiagnosticPathHeader, pub diagnostic_header: DiagnosticHeader, @@ -693,7 +715,7 @@ pub struct Editor { pub whitespace: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Scrollbar { pub track: ContainerStyle, pub thumb: ContainerStyle, @@ -703,14 +725,14 @@ pub struct Scrollbar { pub selections: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct GitDiffColors { pub inserted: Color, pub modified: Color, pub deleted: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct DiagnosticPathHeader { #[serde(flatten)] pub container: ContainerStyle, @@ -719,7 +741,7 @@ pub struct DiagnosticPathHeader { pub text_scale_factor: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct DiagnosticHeader { #[serde(flatten)] pub container: ContainerStyle, @@ -730,7 +752,7 @@ pub struct DiagnosticHeader { pub icon_width_factor: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct DiagnosticStyle { pub message: LabelStyle, #[serde(default)] @@ -738,7 +760,7 @@ pub struct DiagnosticStyle { pub text_scale_factor: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct AutocompleteStyle { #[serde(flatten)] pub container: ContainerStyle, @@ -748,13 +770,13 @@ pub struct AutocompleteStyle { pub match_highlight: HighlightStyle, } -#[derive(Clone, Copy, Default, Deserialize)] +#[derive(Clone, Copy, Default, Deserialize, JsonSchema)] pub struct SelectionStyle { pub cursor: Color, pub selection: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct FieldEditor { #[serde(flatten)] pub container: ContainerStyle, @@ -764,21 +786,21 @@ pub struct FieldEditor { pub selection: SelectionStyle, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct InteractiveColor { pub color: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct CodeActions { #[serde(default)] - pub indicator: Interactive, + pub indicator: Toggleable>, pub vertical_scale: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Folds { - pub indicator: Interactive, + pub indicator: Toggleable>, pub ellipses: FoldEllipses, pub fold_background: Color, pub icon_margin_scale: f32, @@ -786,14 +808,14 @@ pub struct Folds { pub foldable_icon: String, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct FoldEllipses { pub text_color: Color, pub background: Interactive, pub corner_radius_factor: f32, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct DiffStyle { pub inserted: Color, pub modified: Color, @@ -803,41 +825,49 @@ pub struct DiffStyle { pub corner_radius: f32, } -#[derive(Debug, Default, Clone, Copy)] +#[derive(Debug, Default, Clone, Copy, JsonSchema)] pub struct Interactive { pub default: T, - pub hover: Option, - pub hover_and_active: Option, + pub hovered: Option, pub clicked: Option, - pub click_and_active: Option, - pub active: Option, pub disabled: Option, } -impl Interactive { - pub fn style_for(&self, state: &mut MouseState, active: bool) -> &T { +#[derive(Clone, Copy, Debug, Default, Deserialize, JsonSchema)] +pub struct Toggleable { + active: T, + inactive: T, +} + +impl Toggleable { + pub fn new(active: T, inactive: T) -> Self { + Self { active, inactive } + } + pub fn in_state(&self, active: bool) -> &T { if active { - if state.hovered() { - self.hover_and_active - .as_ref() - .unwrap_or(self.active.as_ref().unwrap_or(&self.default)) - } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() - { - self.click_and_active - .as_ref() - .unwrap_or(self.active.as_ref().unwrap_or(&self.default)) - } else { - self.active.as_ref().unwrap_or(&self.default) - } - } else if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() { + &self.active + } else { + &self.inactive + } + } + pub fn active_state(&self) -> &T { + self.in_state(true) + } + pub fn inactive_state(&self) -> &T { + self.in_state(false) + } +} + +impl Interactive { + pub fn style_for(&self, state: &mut MouseState) -> &T { + if state.clicked() == Some(platform::MouseButton::Left) && self.clicked.is_some() { self.clicked.as_ref().unwrap() } else if state.hovered() { - self.hover.as_ref().unwrap_or(&self.default) + self.hovered.as_ref().unwrap_or(&self.default) } else { &self.default } } - pub fn disabled_style(&self) -> &T { self.disabled.as_ref().unwrap_or(&self.default) } @@ -850,13 +880,9 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { { #[derive(Deserialize)] struct Helper { - #[serde(flatten)] default: Value, - hover: Option, - hover_and_active: Option, + hovered: Option, clicked: Option, - click_and_active: Option, - active: Option, disabled: Option, } @@ -881,21 +907,15 @@ impl<'de, T: DeserializeOwned> Deserialize<'de> for Interactive { } }; - let hover = deserialize_state(json.hover)?; - let hover_and_active = deserialize_state(json.hover_and_active)?; + let hovered = deserialize_state(json.hovered)?; let clicked = deserialize_state(json.clicked)?; - let click_and_active = deserialize_state(json.click_and_active)?; - let active = deserialize_state(json.active)?; let disabled = deserialize_state(json.disabled)?; let default = serde_json::from_value(json.default).map_err(serde::de::Error::custom)?; Ok(Interactive { default, - hover, - hover_and_active, + hovered, clicked, - click_and_active, - active, disabled, }) } @@ -912,7 +932,7 @@ impl Editor { } } -#[derive(Default)] +#[derive(Default, JsonSchema)] pub struct SyntaxTheme { pub highlights: Vec<(String, HighlightStyle)>, } @@ -946,7 +966,7 @@ impl<'de> Deserialize<'de> for SyntaxTheme { } } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct HoverPopover { pub container: ContainerStyle, pub info_container: ContainerStyle, @@ -958,7 +978,7 @@ pub struct HoverPopover { pub highlight: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct TerminalStyle { pub black: Color, pub red: Color, @@ -992,24 +1012,39 @@ pub struct TerminalStyle { pub dim_foreground: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct AssistantStyle { pub container: ContainerStyle, - pub header: ContainerStyle, + pub hamburger_button: Interactive, + pub split_button: Interactive, + pub assist_button: Interactive, + pub quote_button: Interactive, + pub zoom_in_button: Interactive, + pub zoom_out_button: Interactive, + pub plus_button: Interactive, + pub title: ContainedText, + pub message_header: ContainerStyle, pub sent_at: ContainedText, pub user_sender: Interactive, pub assistant_sender: Interactive, pub system_sender: Interactive, - pub model_info_container: ContainerStyle, pub model: Interactive, pub remaining_tokens: ContainedText, pub no_remaining_tokens: ContainedText, pub error_icon: Icon, pub api_key_editor: FieldEditor, pub api_key_prompt: ContainedText, + pub saved_conversation: SavedConversation, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] +pub struct SavedConversation { + pub container: Interactive, + pub saved_at: ContainedText, + pub title: ContainedText, +} + +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct FeedbackStyle { pub submit_button: Interactive, pub button_margin: f32, @@ -1018,7 +1053,7 @@ pub struct FeedbackStyle { pub link_text_hover: ContainedText, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct WelcomeStyle { pub page_width: f32, pub logo: SvgStyle, @@ -1032,7 +1067,7 @@ pub struct WelcomeStyle { pub checkbox_group: ContainerStyle, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct ColorScheme { pub name: String, pub is_light: bool, @@ -1047,13 +1082,13 @@ pub struct ColorScheme { pub players: Vec, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Player { pub cursor: Color, pub selection: Color, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct RampSet { pub neutral: Vec, pub red: Vec, @@ -1066,7 +1101,7 @@ pub struct RampSet { pub magenta: Vec, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Layer { pub base: StyleSet, pub variant: StyleSet, @@ -1077,7 +1112,7 @@ pub struct Layer { pub negative: StyleSet, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct StyleSet { pub default: Style, pub active: Style, @@ -1087,7 +1122,7 @@ pub struct StyleSet { pub inverted: Style, } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct Style { pub background: Color, pub border: Color, diff --git a/crates/theme/src/theme_settings.rs b/crates/theme/src/theme_settings.rs index f86d3fd8dd..b9e6f7a133 100644 --- a/crates/theme/src/theme_settings.rs +++ b/crates/theme/src/theme_settings.rs @@ -14,12 +14,13 @@ use util::ResultExt as _; const MIN_FONT_SIZE: f32 = 6.0; -#[derive(Clone)] +#[derive(Clone, JsonSchema)] pub struct ThemeSettings { pub buffer_font_family_name: String, pub buffer_font_features: fonts::Features, pub buffer_font_family: FamilyId, pub(crate) buffer_font_size: f32, + #[serde(skip)] pub theme: Arc, } diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index b86bfca8c4..308ea6f2d7 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -1,23 +1,23 @@ use std::borrow::Cow; use gpui::{ - color::Color, elements::{ - ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label, - MouseEventHandler, ParentElement, Stack, Svg, + ConstrainedBox, Container, ContainerStyle, Dimensions, Empty, Flex, KeystrokeLabel, Label, + MouseEventHandler, ParentElement, Stack, Svg, SvgStyle, }, fonts::TextStyle, - geometry::vector::{vec2f, Vector2F}, + geometry::vector::Vector2F, platform, platform::MouseButton, scene::MouseClick, Action, Element, EventContext, MouseState, View, ViewContext, }; +use schemars::JsonSchema; use serde::Deserialize; use crate::{ContainedText, Interactive}; -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct CheckboxStyle { pub icon: SvgStyle, pub label: ContainedText, @@ -93,25 +93,6 @@ where .with_cursor_style(platform::CursorStyle::PointingHand) } -#[derive(Clone, Deserialize, Default)] -pub struct SvgStyle { - pub color: Color, - pub asset: String, - pub dimensions: Dimensions, -} - -#[derive(Clone, Deserialize, Default)] -pub struct Dimensions { - pub width: f32, - pub height: f32, -} - -impl Dimensions { - pub fn to_vec(&self) -> Vector2F { - vec2f(self.width, self.height) - } -} - pub fn svg(style: &SvgStyle) -> ConstrainedBox { Svg::new(style.asset.clone()) .with_color(style.color) @@ -120,10 +101,10 @@ pub fn svg(style: &SvgStyle) -> ConstrainedBox { .with_height(style.dimensions.height) } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct IconStyle { - icon: SvgStyle, - container: ContainerStyle, + pub icon: SvgStyle, + pub container: ContainerStyle, } pub fn icon(style: &IconStyle) -> Container { @@ -170,7 +151,7 @@ where F: Fn(MouseClick, &mut V, &mut EventContext) + 'static, { MouseEventHandler::::new(0, cx, |state, _| { - let style = style.style_for(state, false); + let style = style.style_for(state); Label::new(label, style.text.to_owned()) .aligned() .contained() @@ -182,7 +163,7 @@ where .with_cursor_style(platform::CursorStyle::PointingHand) } -#[derive(Clone, Deserialize, Default)] +#[derive(Clone, Deserialize, Default, JsonSchema)] pub struct ModalStyle { close_icon: Interactive, container: ContainerStyle, @@ -220,13 +201,13 @@ where title, style .title_text - .style_for(&mut MouseState::default(), false) + .style_for(&mut MouseState::default()) .clone(), )) .with_child( // FIXME: Get a better tag type MouseEventHandler::::new(999999, cx, |state, _cx| { - let style = style.close_icon.style_for(state, false); + let style = style.close_icon.style_for(state); icon(style) }) .on_click(platform::MouseButton::Left, move |_, _, cx| { diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index a6c84d1d91..5775f1b3e7 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -208,7 +208,7 @@ impl PickerDelegate for ThemeSelectorDelegate { cx: &AppContext, ) -> AnyElement> { let theme = theme::current(cx); - let style = theme.picker.item.style_for(mouse_state, selected); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); let theme_match = &self.matches[ix]; Label::new(theme_match.string.clone(), style.label.clone()) diff --git a/crates/theme_testbench/Cargo.toml b/crates/theme_testbench/Cargo.toml deleted file mode 100644 index 32dca6a07a..0000000000 --- a/crates/theme_testbench/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "theme_testbench" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/theme_testbench.rs" -doctest = false - - -[dependencies] -gpui = { path = "../gpui" } -theme = { path = "../theme" } -settings = { path = "../settings" } -workspace = { path = "../workspace" } -project = { path = "../project" } - -smallvec.workspace = true diff --git a/crates/theme_testbench/src/theme_testbench.rs b/crates/theme_testbench/src/theme_testbench.rs deleted file mode 100644 index 258249b599..0000000000 --- a/crates/theme_testbench/src/theme_testbench.rs +++ /dev/null @@ -1,300 +0,0 @@ -use gpui::{ - actions, - color::Color, - elements::{ - AnyElement, Canvas, Container, ContainerStyle, Flex, Label, Margin, MouseEventHandler, - Padding, ParentElement, - }, - fonts::TextStyle, - AppContext, Border, Element, Entity, ModelHandle, Quad, Task, View, ViewContext, ViewHandle, - WeakViewHandle, -}; -use project::Project; -use theme::{ColorScheme, Layer, Style, StyleSet, ThemeSettings}; -use workspace::{item::Item, register_deserializable_item, Pane, Workspace}; - -actions!(theme, [DeployThemeTestbench]); - -pub fn init(cx: &mut AppContext) { - cx.add_action(ThemeTestbench::deploy); - - register_deserializable_item::(cx) -} - -pub struct ThemeTestbench {} - -impl ThemeTestbench { - pub fn deploy( - workspace: &mut Workspace, - _: &DeployThemeTestbench, - cx: &mut ViewContext, - ) { - let view = cx.add_view(|_| ThemeTestbench {}); - workspace.add_item(Box::new(view), cx); - } - - fn render_ramps(color_scheme: &ColorScheme) -> Flex { - fn display_ramp(ramp: &Vec) -> AnyElement { - Flex::row() - .with_children(ramp.iter().cloned().map(|color| { - Canvas::new(move |scene, bounds, _, _, _| { - scene.push_quad(Quad { - bounds, - background: Some(color), - ..Default::default() - }); - }) - .flex(1.0, false) - })) - .flex(1.0, false) - .into_any() - } - - Flex::column() - .with_child(display_ramp(&color_scheme.ramps.neutral)) - .with_child(display_ramp(&color_scheme.ramps.red)) - .with_child(display_ramp(&color_scheme.ramps.orange)) - .with_child(display_ramp(&color_scheme.ramps.yellow)) - .with_child(display_ramp(&color_scheme.ramps.green)) - .with_child(display_ramp(&color_scheme.ramps.cyan)) - .with_child(display_ramp(&color_scheme.ramps.blue)) - .with_child(display_ramp(&color_scheme.ramps.violet)) - .with_child(display_ramp(&color_scheme.ramps.magenta)) - } - - fn render_layer( - layer_index: usize, - layer: &Layer, - cx: &mut ViewContext, - ) -> Container { - Flex::column() - .with_child( - Self::render_button_set(0, layer_index, "base", &layer.base, cx).flex(1., false), - ) - .with_child( - Self::render_button_set(1, layer_index, "variant", &layer.variant, cx) - .flex(1., false), - ) - .with_child( - Self::render_button_set(2, layer_index, "on", &layer.on, cx).flex(1., false), - ) - .with_child( - Self::render_button_set(3, layer_index, "accent", &layer.accent, cx) - .flex(1., false), - ) - .with_child( - Self::render_button_set(4, layer_index, "positive", &layer.positive, cx) - .flex(1., false), - ) - .with_child( - Self::render_button_set(5, layer_index, "warning", &layer.warning, cx) - .flex(1., false), - ) - .with_child( - Self::render_button_set(6, layer_index, "negative", &layer.negative, cx) - .flex(1., false), - ) - .contained() - .with_style(ContainerStyle { - margin: Margin { - top: 10., - bottom: 10., - left: 10., - right: 10., - }, - background_color: Some(layer.base.default.background), - ..Default::default() - }) - } - - fn render_button_set( - set_index: usize, - layer_index: usize, - set_name: &'static str, - style_set: &StyleSet, - cx: &mut ViewContext, - ) -> Flex { - Flex::row() - .with_child(Self::render_button( - set_index * 6, - layer_index, - set_name, - &style_set, - None, - cx, - )) - .with_child(Self::render_button( - set_index * 6 + 1, - layer_index, - "hovered", - &style_set, - Some(|style_set| &style_set.hovered), - cx, - )) - .with_child(Self::render_button( - set_index * 6 + 2, - layer_index, - "pressed", - &style_set, - Some(|style_set| &style_set.pressed), - cx, - )) - .with_child(Self::render_button( - set_index * 6 + 3, - layer_index, - "active", - &style_set, - Some(|style_set| &style_set.active), - cx, - )) - .with_child(Self::render_button( - set_index * 6 + 4, - layer_index, - "disabled", - &style_set, - Some(|style_set| &style_set.disabled), - cx, - )) - .with_child(Self::render_button( - set_index * 6 + 5, - layer_index, - "inverted", - &style_set, - Some(|style_set| &style_set.inverted), - cx, - )) - } - - fn render_button( - button_index: usize, - layer_index: usize, - text: &'static str, - style_set: &StyleSet, - style_override: Option &Style>, - cx: &mut ViewContext, - ) -> AnyElement { - enum TestBenchButton {} - MouseEventHandler::::new(layer_index + button_index, cx, |state, cx| { - let style = if let Some(style_override) = style_override { - style_override(&style_set) - } else if state.clicked().is_some() { - &style_set.pressed - } else if state.hovered() { - &style_set.hovered - } else { - &style_set.default - }; - - Self::render_label(text.to_string(), style, cx) - .contained() - .with_style(ContainerStyle { - margin: Margin { - top: 4., - bottom: 4., - left: 4., - right: 4., - }, - padding: Padding { - top: 4., - bottom: 4., - left: 4., - right: 4., - }, - background_color: Some(style.background), - border: Border { - width: 1., - color: style.border, - overlay: false, - top: true, - bottom: true, - left: true, - right: true, - }, - corner_radius: 2., - ..Default::default() - }) - }) - .flex(1., true) - .into_any() - } - - fn render_label(text: String, style: &Style, cx: &mut ViewContext) -> Label { - let settings = settings::get::(cx); - let font_cache = cx.font_cache(); - let family_id = settings.buffer_font_family; - let font_size = settings.buffer_font_size(cx); - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - - let text_style = TextStyle { - color: style.foreground, - font_family_id: family_id, - font_family_name: font_cache.family_name(family_id).unwrap(), - font_id, - font_size, - font_properties: Default::default(), - underline: Default::default(), - }; - - Label::new(text, text_style) - } -} - -impl Entity for ThemeTestbench { - type Event = (); -} - -impl View for ThemeTestbench { - fn ui_name() -> &'static str { - "ThemeTestbench" - } - - fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { - let color_scheme = &theme::current(cx).clone().color_scheme; - - Flex::row() - .with_child( - Self::render_ramps(color_scheme) - .contained() - .with_margin_right(10.) - .flex(0.1, false), - ) - .with_child( - Flex::column() - .with_child(Self::render_layer(100, &color_scheme.lowest, cx).flex(1., true)) - .with_child(Self::render_layer(200, &color_scheme.middle, cx).flex(1., true)) - .with_child(Self::render_layer(300, &color_scheme.highest, cx).flex(1., true)) - .flex(1., false), - ) - .into_any() - } -} - -impl Item for ThemeTestbench { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - _: &AppContext, - ) -> AnyElement { - Label::new("Theme Testbench", style.label.clone()) - .aligned() - .contained() - .into_any() - } - - fn serialized_item_kind() -> Option<&'static str> { - Some("ThemeTestBench") - } - - fn deserialize( - _project: ModelHandle, - _workspace: WeakViewHandle, - _workspace_id: workspace::WorkspaceId, - _item_id: workspace::ItemId, - cx: &mut ViewContext, - ) -> Task>> { - Task::ready(Ok(cx.add_view(|_| Self {}))) - } -} diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index e3397a1557..7ef55a9918 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; lazy_static::lazy_static! { pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); + pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations"); pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages"); diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 71ffacebf3..c8beb86aef 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -118,14 +118,15 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se } } -pub trait ResultExt { +pub trait ResultExt { type Ok; fn log_err(self) -> Option; fn warn_on_err(self) -> Option; + fn inspect_error(self, func: impl FnOnce(&E)) -> Self; } -impl ResultExt for Result +impl ResultExt for Result where E: std::fmt::Debug, { @@ -152,6 +153,15 @@ where } } } + + /// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err + fn inspect_error(self, func: impl FnOnce(&E)) -> Self { + if let Err(err) = &self { + func(err); + } + + self + } } pub trait TryFutureExt { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index cc3ec06c47..faf69d9473 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -209,8 +209,9 @@ impl Motion { map: &DisplaySnapshot, point: DisplayPoint, goal: SelectionGoal, - times: usize, + maybe_times: Option, ) -> Option<(DisplayPoint, SelectionGoal)> { + let times = maybe_times.unwrap_or(1); use Motion::*; let infallible = self.infallible(); let (new_point, goal) = match self { @@ -236,7 +237,10 @@ impl Motion { EndOfLine => (end_of_line(map, point), SelectionGoal::None), CurrentLine => (end_of_line(map, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), - EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None), + EndOfDocument => ( + end_of_document(map, point, maybe_times), + SelectionGoal::None, + ), Matching => (matching(map, point), SelectionGoal::None), FindForward { before, text } => ( find_forward(map, point, *before, text.clone(), times), @@ -257,7 +261,7 @@ impl Motion { &self, map: &DisplaySnapshot, selection: &mut Selection, - times: usize, + times: Option, expand_to_surrounding_newline: bool, ) -> bool { if let Some((new_head, goal)) = @@ -473,14 +477,19 @@ fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> map.clip_point(new_point, Bias::Left) } -fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint { - let mut new_point = if line == 1 { - map.max_point() +fn end_of_document( + map: &DisplaySnapshot, + point: DisplayPoint, + line: Option, +) -> DisplayPoint { + let new_row = if let Some(line) = line { + (line - 1) as u32 } else { - Point::new((line - 1) as u32, 0).to_display_point(map) + map.max_buffer_row() }; - *new_point.column_mut() = point.column(); - map.clip_point(new_point, Bias::Left) + + let new_point = Point::new(new_row, point.column()); + map.clip_point(new_point.to_display_point(map), Bias::Left) } fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 1f90d259d3..1227afbb85 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -1,8 +1,11 @@ +mod case; mod change; mod delete; +mod scroll; +mod substitute; mod yank; -use std::{borrow::Cow, cmp::Ordering, sync::Arc}; +use std::{borrow::Cow, sync::Arc}; use crate::{ motion::Motion, @@ -12,25 +15,22 @@ use crate::{ }; use collections::{HashMap, HashSet}; use editor::{ - display_map::ToDisplayPoint, - scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, - Anchor, Bias, ClipboardSelection, DisplayPoint, Editor, + display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Anchor, Bias, ClipboardSelection, + DisplayPoint, }; -use gpui::{actions, impl_actions, AppContext, ViewContext, WindowContext}; +use gpui::{actions, AppContext, ViewContext, WindowContext}; use language::{AutoindentMode, Point, SelectionGoal}; use log::error; -use serde::Deserialize; use workspace::Workspace; use self::{ + case::change_case, change::{change_motion, change_object}, delete::{delete_motion, delete_object}, + substitute::substitute, yank::{yank_motion, yank_object}, }; -#[derive(Clone, PartialEq, Deserialize)] -struct Scroll(ScrollAmount); - actions!( vim, [ @@ -45,17 +45,24 @@ actions!( DeleteToEndOfLine, Paste, Yank, + Substitute, + ChangeCase, ] ); -impl_actions!(vim, [Scroll]); - pub fn init(cx: &mut AppContext) { cx.add_action(insert_after); cx.add_action(insert_first_non_whitespace); cx.add_action(insert_end_of_line); cx.add_action(insert_line_above); cx.add_action(insert_line_below); + cx.add_action(change_case); + cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { + Vim::update(cx, |vim, cx| { + let times = vim.pop_number_operator(cx); + substitute(vim, times, cx); + }) + }); cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); @@ -81,19 +88,14 @@ pub fn init(cx: &mut AppContext) { }) }); cx.add_action(paste); - cx.add_action(|_: &mut Workspace, Scroll(amount): &Scroll, cx| { - Vim::update(cx, |vim, cx| { - vim.update_active_editor(cx, |editor, cx| { - scroll(editor, amount, cx); - }) - }) - }); + + scroll::init(cx); } pub fn normal_motion( motion: Motion, operator: Option, - times: usize, + times: Option, cx: &mut WindowContext, ) { Vim::update(cx, |vim, cx| { @@ -129,7 +131,7 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) { }) } -fn move_cursor(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) { +fn move_cursor(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_cursors_with(|map, cursor, goal| { @@ -147,7 +149,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext) { }); } -fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext) { - let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq(); - editor.scroll_screen(amount, cx); - if should_move_cursor { - let selection_ordering = editor.newest_selection_on_screen(cx); - if selection_ordering.is_eq() { - return; - } - - let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { - visible_rows as u32 - } else { - return; - }; - - let scroll_margin_rows = editor.vertical_scroll_margin() as u32; - let top_anchor = editor.scroll_manager.anchor().anchor; - - editor.change_selections(None, cx, |s| { - s.replace_cursors_with(|snapshot| { - let mut new_point = top_anchor.to_display_point(&snapshot); - - match selection_ordering { - Ordering::Less => { - *new_point.row_mut() += scroll_margin_rows; - new_point = snapshot.clip_point(new_point, Bias::Right); - } - Ordering::Greater => { - *new_point.row_mut() += visible_rows - scroll_margin_rows as u32; - new_point = snapshot.clip_point(new_point, Bias::Left); - } - Ordering::Equal => unreachable!(), - } - - vec![new_point] - }) - }); - } -} - pub(crate) fn normal_replace(text: Arc, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs new file mode 100644 index 0000000000..ba527af0bb --- /dev/null +++ b/crates/vim/src/normal/case.rs @@ -0,0 +1,64 @@ +use gpui::ViewContext; +use language::Point; +use workspace::Workspace; + +use crate::{motion::Motion, normal::ChangeCase, Vim}; + +pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + let count = vim.pop_number_operator(cx); + vim.update_active_editor(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.transact(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if selection.start == selection.end { + Motion::Right.expand_selection(map, selection, count, true); + } + }) + }); + let selections = editor.selections.all::(cx); + for selection in selections.into_iter().rev() { + let snapshot = editor.buffer().read(cx).snapshot(cx); + editor.buffer().update(cx, |buffer, cx| { + let range = selection.start..selection.end; + let text = snapshot + .text_for_range(selection.start..selection.end) + .flat_map(|s| s.chars()) + .flat_map(|c| { + if c.is_lowercase() { + c.to_uppercase().collect::>() + } else { + c.to_lowercase().collect::>() + } + }) + .collect::(); + + buffer.edit([(range, text)], None, cx) + }) + } + }); + editor.set_clip_at_line_ends(true, cx); + }); + }) +} + +#[cfg(test)] +mod test { + use crate::{state::Mode, test::VimTestContext}; + use indoc::indoc; + + #[gpui::test] + async fn test_change_case(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + cx.set_state(indoc! {"ˇabC\n"}, Mode::Normal); + cx.simulate_keystrokes(["~"]); + cx.assert_editor_state("AˇbC\n"); + cx.simulate_keystrokes(["2", "~"]); + cx.assert_editor_state("ABcˇ\n"); + + cx.set_state(indoc! {"a😀C«dÉ1*fˇ»\n"}, Mode::Normal); + cx.simulate_keystrokes(["~"]); + cx.assert_editor_state("a😀CDé1*Fˇ\n"); + } +} diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 350ffdbb8f..d226c70410 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -6,7 +6,7 @@ use editor::{ use gpui::WindowContext; use language::Selection; -pub fn change_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) { +pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { // Some motions ignore failure when switching to normal mode let mut motion_succeeded = matches!( motion, @@ -78,10 +78,10 @@ pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut Windo fn expand_changed_word_selection( map: &DisplaySnapshot, selection: &mut Selection, - times: usize, + times: Option, ignore_punctuation: bool, ) -> bool { - if times == 1 { + if times.is_none() || times.unwrap() == 1 { let in_word = map .chars_at(selection.head()) .next() @@ -97,7 +97,8 @@ fn expand_changed_word_selection( }); true } else { - Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, 1, false) + Motion::NextWordStart { ignore_punctuation } + .expand_selection(map, selection, None, false) } } else { Motion::NextWordStart { ignore_punctuation }.expand_selection(map, selection, times, false) diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index cbea65ddaf..56fef78e1d 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -3,7 +3,7 @@ use collections::{HashMap, HashSet}; use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias}; use gpui::WindowContext; -pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) { +pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs new file mode 100644 index 0000000000..7b068cd793 --- /dev/null +++ b/crates/vim/src/normal/scroll.rs @@ -0,0 +1,120 @@ +use std::cmp::Ordering; + +use crate::Vim; +use editor::{display_map::ToDisplayPoint, scroll::scroll_amount::ScrollAmount, Editor}; +use gpui::{actions, AppContext, ViewContext}; +use language::Bias; +use workspace::Workspace; + +actions!( + vim, + [LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown,] +); + +pub fn init(cx: &mut AppContext) { + cx.add_action(|_: &mut Workspace, _: &LineDown, cx| { + scroll(cx, |c| ScrollAmount::Line(c.unwrap_or(1.))) + }); + cx.add_action(|_: &mut Workspace, _: &LineUp, cx| { + scroll(cx, |c| ScrollAmount::Line(-c.unwrap_or(1.))) + }); + cx.add_action(|_: &mut Workspace, _: &PageDown, cx| { + scroll(cx, |c| ScrollAmount::Page(c.unwrap_or(1.))) + }); + cx.add_action(|_: &mut Workspace, _: &PageUp, cx| { + scroll(cx, |c| ScrollAmount::Page(-c.unwrap_or(1.))) + }); + cx.add_action(|_: &mut Workspace, _: &ScrollDown, cx| { + scroll(cx, |c| { + if let Some(c) = c { + ScrollAmount::Line(c) + } else { + ScrollAmount::Page(0.5) + } + }) + }); + cx.add_action(|_: &mut Workspace, _: &ScrollUp, cx| { + scroll(cx, |c| { + if let Some(c) = c { + ScrollAmount::Line(-c) + } else { + ScrollAmount::Page(-0.5) + } + }) + }); +} + +fn scroll(cx: &mut ViewContext, by: fn(c: Option) -> ScrollAmount) { + Vim::update(cx, |vim, cx| { + let amount = by(vim.pop_number_operator(cx).map(|c| c as f32)); + vim.update_active_editor(cx, |editor, cx| scroll_editor(editor, &amount, cx)); + }) +} + +fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext) { + let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq(); + editor.scroll_screen(amount, cx); + if should_move_cursor { + let selection_ordering = editor.newest_selection_on_screen(cx); + if selection_ordering.is_eq() { + return; + } + + let visible_rows = if let Some(visible_rows) = editor.visible_line_count() { + visible_rows as u32 + } else { + return; + }; + + let top_anchor = editor.scroll_manager.anchor().anchor; + + editor.change_selections(None, cx, |s| { + s.replace_cursors_with(|snapshot| { + let mut new_point = top_anchor.to_display_point(&snapshot); + + match selection_ordering { + Ordering::Less => { + new_point = snapshot.clip_point(new_point, Bias::Right); + } + Ordering::Greater => { + *new_point.row_mut() += visible_rows - 1; + new_point = snapshot.clip_point(new_point, Bias::Left); + } + Ordering::Equal => unreachable!(), + } + + vec![new_point] + }) + }); + } +} + +#[cfg(test)] +mod test { + use crate::{state::Mode, test::VimTestContext}; + use gpui::geometry::vector::vec2f; + use indoc::indoc; + + #[gpui::test] + async fn test_scroll(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state(indoc! {"ˇa\nb\nc\nd\ne\n"}, Mode::Normal); + + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.)) + }); + cx.simulate_keystrokes(["ctrl-e"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.)) + }); + cx.simulate_keystrokes(["2", "ctrl-e"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.)) + }); + cx.simulate_keystrokes(["ctrl-y"]); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.)) + }); + } +} diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs new file mode 100644 index 0000000000..ef72baae31 --- /dev/null +++ b/crates/vim/src/normal/substitute.rs @@ -0,0 +1,73 @@ +use gpui::WindowContext; +use language::Point; + +use crate::{motion::Motion, Mode, Vim}; + +pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { + vim.update_active_editor(cx, |editor, cx| { + editor.set_clip_at_line_ends(false, cx); + editor.transact(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.move_with(|map, selection| { + if selection.start == selection.end { + Motion::Right.expand_selection(map, selection, count, true); + } + }) + }); + let selections = editor.selections.all::(cx); + for selection in selections.into_iter().rev() { + editor.buffer().update(cx, |buffer, cx| { + buffer.edit([(selection.start..selection.end, "")], None, cx) + }) + } + }); + editor.set_clip_at_line_ends(true, cx); + }); + vim.switch_mode(Mode::Insert, true, cx) +} + +#[cfg(test)] +mod test { + use crate::{state::Mode, test::VimTestContext}; + use indoc::indoc; + + #[gpui::test] + async fn test_substitute(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // supports a single cursor + cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); + cx.simulate_keystrokes(["s", "x"]); + cx.assert_editor_state("xˇbc\n"); + + // supports a selection + cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual { line: false }); + cx.assert_editor_state("a«bcˇ»\n"); + cx.simulate_keystrokes(["s", "x"]); + cx.assert_editor_state("axˇ\n"); + + // supports counts + cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); + cx.simulate_keystrokes(["2", "s", "x"]); + cx.assert_editor_state("xˇc\n"); + + // supports multiple cursors + cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal); + cx.simulate_keystrokes(["2", "s", "x"]); + cx.assert_editor_state("axˇdexˇg\n"); + + // does not read beyond end of line + cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); + cx.simulate_keystrokes(["5", "s", "x"]); + cx.assert_editor_state("xˇ\n"); + + // it handles multibyte characters + cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal); + cx.simulate_keystrokes(["4", "s"]); + cx.assert_editor_state("ˇ\n"); + + // should transactionally undo selection changes + cx.simulate_keystrokes(["escape", "u"]); + cx.assert_editor_state("ˇcàfé\n"); + } +} diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index aeef333127..7212a865bd 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -2,7 +2,7 @@ use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim} use collections::HashMap; use gpui::WindowContext; -pub fn yank_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut WindowContext) { +pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &mut WindowContext) { vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { editor.set_clip_at_line_ends(false, cx); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 0214806e11..b6c5b7ca51 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -98,3 +98,44 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) { assert_eq!(bar.query_editor.read(cx).text(cx), "jumps"); }) } + +#[gpui::test] +async fn test_count_down(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal); + cx.simulate_keystrokes(["2", "down"]); + cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee"); + cx.simulate_keystrokes(["9", "down"]); + cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe"); +} + +#[gpui::test] +async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // goes to end by default + cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal); + cx.simulate_keystrokes(["shift-g"]); + cx.assert_editor_state("aa\nbb\ncˇc"); + + // can go to line 1 (https://github.com/zed-industries/community/issues/710) + cx.simulate_keystrokes(["1", "shift-g"]); + cx.assert_editor_state("aˇa\nbb\ncc"); +} + +#[gpui::test] +async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // works in normal mode + cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal); + cx.simulate_keystrokes([">", ">"]); + cx.assert_editor_state("aa\n bˇb\ncc"); + cx.simulate_keystrokes(["<", "<"]); + cx.assert_editor_state("aa\nbˇb\ncc"); + + // works in visuial mode + cx.simulate_keystrokes(["shift-v", "down", ">", ">"]); + cx.assert_editor_state("aa\n b«b\n cˇ»c"); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index d10ec5e824..eae8643cf3 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -238,13 +238,12 @@ impl Vim { popped_operator } - fn pop_number_operator(&mut self, cx: &mut WindowContext) -> usize { - let mut times = 1; + fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option { if let Some(Operator::Number(number)) = self.active_operator() { - times = number; self.pop_operator(cx); + return Some(number); } - times + None } fn clear_operator(&mut self, cx: &mut WindowContext) { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index c3728db222..5e22e77bf0 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(paste); } -pub fn visual_motion(motion: Motion, times: usize, cx: &mut WindowContext) { +pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index e44b391d84..cf24a9127e 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -141,7 +141,7 @@ impl PickerDelegate for BaseKeymapSelectorDelegate { ) -> gpui::AnyElement> { let theme = &theme::current(cx); let keymap_match = &self.matches[ix]; - let style = theme.picker.item.style_for(mouse_state, selected); + let style = theme.picker.item.in_state(selected).style_for(mouse_state); Label::new(keymap_match.string.clone(), style.label.clone()) .with_highlights(keymap_match.positions.clone()) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 48f486381d..ebaf399e22 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -249,7 +249,7 @@ impl Dock { } } - pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + pub(crate) fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { let subscriptions = [ cx.observe(&panel, |_, _, cx| cx.notify()), cx.subscribe(&panel, |this, panel, event, cx| { @@ -498,7 +498,9 @@ impl View for PanelButtons { Stack::new() .with_child( MouseEventHandler::::new(panel_ix, cx, |state, cx| { - let style = button_style.style_for(state, is_active); + let style = button_style.in_state(is_active); + + let style = style.style_for(state); Flex::row() .with_child( Svg::new(view.icon_path(cx)) @@ -598,11 +600,12 @@ impl StatusItemView for PanelButtons { } } -#[cfg(test)] -pub(crate) mod test { +#[cfg(any(test, feature = "test-support"))] +pub mod test { use super::*; use gpui::{ViewContext, WindowContext}; + #[derive(Debug)] pub enum TestPanelEvent { PositionChanged, Activated, diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 9a3fb5e475..a3e3ab9299 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -710,8 +710,8 @@ impl FollowableItemHandle for ViewHandle { } } -#[cfg(test)] -pub(crate) mod test { +#[cfg(any(test, feature = "test-support"))] +pub mod test { use super::{Item, ItemEvent}; use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; use gpui::{ diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 1e3c6044a1..09cfb4d5d8 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -291,7 +291,7 @@ pub mod simple_message_notification { ) .with_child( MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state, false); + let style = theme.dismiss_button.style_for(state); Svg::new("icons/x_mark_8.svg") .with_color(style.color) .constrained() @@ -323,7 +323,7 @@ pub mod simple_message_notification { 0, cx, |state, _| { - let style = theme.action_message.style_for(state, false); + let style = theme.action_message.style_for(state); Flex::row() .with_child( diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 551bc831d3..6a20fab9a2 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,9 +1,10 @@ mod dragged_item_receiver; use super::{ItemHandle, SplitDirection}; +pub use crate::toolbar::Toolbar; use crate::{ - item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item, - NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings, + item::WeakItemHandle, notify_of_new_dock, AutosaveSetting, Item, NewCenterTerminal, NewFile, + NewSearch, ToggleZoom, Workspace, WorkspaceSettings, }; use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; @@ -250,7 +251,7 @@ impl Pane { pane: handle.clone(), next_timestamp, }))), - toolbar: cx.add_view(|_| Toolbar::new(handle)), + toolbar: cx.add_view(|_| Toolbar::new(Some(handle))), tab_bar_context_menu: TabBarContextMenu { kind: TabBarContextMenuKind::New, handle: context_menu, @@ -272,6 +273,11 @@ impl Pane { Some(("New...".into(), None)), cx, |pane, cx| pane.deploy_new_menu(cx), + |pane, cx| { + pane.tab_bar_context_menu + .handle + .update(cx, |menu, _| menu.delay_cancel()) + }, pane.tab_bar_context_menu .handle_if_kind(TabBarContextMenuKind::New), )) @@ -282,22 +288,36 @@ impl Pane { Some(("Split Pane".into(), None)), cx, |pane, cx| pane.deploy_split_menu(cx), + |pane, cx| { + pane.tab_bar_context_menu + .handle + .update(cx, |menu, _| menu.delay_cancel()) + }, pane.tab_bar_context_menu .handle_if_kind(TabBarContextMenuKind::Split), )) - .with_child(Pane::render_tab_bar_button( - 2, + .with_child({ + let icon_path; + let tooltip_label; if pane.is_zoomed() { - "icons/minimize_8.svg" + icon_path = "icons/minimize_8.svg"; + tooltip_label = "Zoom In".into(); } else { - "icons/maximize_8.svg" - }, - pane.is_zoomed(), - Some(("Toggle Zoom".into(), Some(Box::new(ToggleZoom)))), - cx, - move |pane, cx| pane.toggle_zoom(&Default::default(), cx), - None, - )) + icon_path = "icons/maximize_8.svg"; + tooltip_label = "Zoom In".into(); + } + + Pane::render_tab_bar_button( + 2, + icon_path, + pane.is_zoomed(), + Some((tooltip_label, Some(Box::new(ToggleZoom)))), + cx, + move |pane, cx| pane.toggle_zoom(&Default::default(), cx), + move |_, _| {}, + None, + ) + }) .into_any() }), } @@ -979,7 +999,7 @@ impl Pane { fn deploy_split_menu(&mut self, cx: &mut ViewContext) { self.tab_bar_context_menu.handle.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::TopRight, vec![ @@ -997,7 +1017,7 @@ impl Pane { fn deploy_new_menu(&mut self, cx: &mut ViewContext) { self.tab_bar_context_menu.handle.update(cx, |menu, cx| { - menu.show( + menu.toggle( Default::default(), AnchorCorner::TopRight, vec![ @@ -1112,7 +1132,7 @@ impl Pane { .get(self.active_item_index) .map(|item| item.as_ref()); self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_pane_item(active_item, cx); + toolbar.set_active_item(active_item, cx); }); } @@ -1407,20 +1427,24 @@ impl Pane { .into_any() } - pub fn render_tab_bar_button)>( + pub fn render_tab_bar_button< + F1: 'static + Fn(&mut Pane, &mut EventContext), + F2: 'static + Fn(&mut Pane, &mut EventContext), + >( index: usize, icon: &'static str, - active: bool, + is_active: bool, tooltip: Option<(String, Option>)>, cx: &mut ViewContext, - on_click: F, + on_click: F1, + on_down: F2, context_menu: Option>, ) -> AnyElement { enum TabBarButton {} let mut button = MouseEventHandler::::new(index, cx, |mouse_state, cx| { let theme = &settings::get::(cx).theme.workspace.tab_bar; - let style = theme.pane_button.style_for(mouse_state, active); + let style = theme.pane_button.in_state(is_active).style_for(mouse_state); Svg::new(icon) .with_color(style.color) .constrained() @@ -1431,6 +1455,7 @@ impl Pane { .with_height(style.button_width) }) .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx)) .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) .into_any(); if let Some((tooltip, action)) = tooltip { @@ -1602,7 +1627,7 @@ impl View for Pane { } self.toolbar.update(cx, |toolbar, cx| { - toolbar.pane_focus_update(true, cx); + toolbar.focus_changed(true, cx); }); if let Some(active_item) = self.active_item() { @@ -1631,7 +1656,7 @@ impl View for Pane { fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { self.has_focus = false; self.toolbar.update(cx, |toolbar, cx| { - toolbar.pane_focus_update(false, cx); + toolbar.focus_changed(false, cx); }); cx.notify(); } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index d27818d202..dd2aa5a818 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -162,6 +162,12 @@ define_connection! { ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool )]; } @@ -196,10 +202,13 @@ impl WorkspaceDb { display, left_dock_visible, left_dock_active_panel, + left_dock_zoom, right_dock_visible, right_dock_active_panel, + right_dock_zoom, bottom_dock_visible, - bottom_dock_active_panel + bottom_dock_active_panel, + bottom_dock_zoom FROM workspaces WHERE workspace_location = ? }) @@ -244,22 +253,28 @@ impl WorkspaceDb { workspace_location, left_dock_visible, left_dock_active_panel, + left_dock_zoom, right_dock_visible, right_dock_active_panel, + right_dock_zoom, bottom_dock_visible, bottom_dock_active_panel, + bottom_dock_zoom, timestamp ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, CURRENT_TIMESTAMP) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP) ON CONFLICT DO UPDATE SET workspace_location = ?2, left_dock_visible = ?3, left_dock_active_panel = ?4, - right_dock_visible = ?5, - right_dock_active_panel = ?6, - bottom_dock_visible = ?7, - bottom_dock_active_panel = ?8, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, timestamp = CURRENT_TIMESTAMP ))?((workspace.id, &workspace.location, workspace.docks)) .context("Updating workspace")?; diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 9e3c4012cd..1075061853 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -100,16 +100,19 @@ impl Bind for DockStructure { pub struct DockData { pub(crate) visible: bool, pub(crate) active_panel: Option, + pub(crate) zoom: bool, } impl Column for DockData { fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { let (visible, next_index) = Option::::column(statement, start_index)?; let (active_panel, next_index) = Option::::column(statement, next_index)?; + let (zoom, next_index) = Option::::column(statement, next_index)?; Ok(( DockData { visible: visible.unwrap_or(false), active_panel, + zoom: zoom.unwrap_or(false), }, next_index, )) @@ -119,7 +122,8 @@ impl Column for DockData { impl Bind for DockData { fn bind(&self, statement: &Statement, start_index: i32) -> Result { let next_index = statement.bind(&self.visible, start_index)?; - statement.bind(&self.active_panel, next_index) + let next_index = statement.bind(&self.active_panel, next_index)?; + statement.bind(&self.zoom, next_index) } } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 8b26b1181b..69394b8421 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -38,7 +38,7 @@ trait ToolbarItemViewHandle { active_pane_item: Option<&dyn ItemHandle>, cx: &mut WindowContext, ) -> ToolbarItemLocation; - fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext); + fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext); fn row_count(&self, cx: &WindowContext) -> usize; } @@ -51,10 +51,10 @@ pub enum ToolbarItemLocation { } pub struct Toolbar { - active_pane_item: Option>, + active_item: Option>, hidden: bool, can_navigate: bool, - pane: WeakViewHandle, + pane: Option>, items: Vec<(Box, ToolbarItemLocation)>, } @@ -121,7 +121,7 @@ impl View for Toolbar { let pane = self.pane.clone(); let mut enable_go_backward = false; let mut enable_go_forward = false; - if let Some(pane) = pane.upgrade(cx) { + if let Some(pane) = pane.and_then(|pane| pane.upgrade(cx)) { let pane = pane.read(cx); enable_go_backward = pane.can_navigate_backward(); enable_go_forward = pane.can_navigate_forward(); @@ -143,19 +143,17 @@ impl View for Toolbar { enable_go_backward, spacing, { - let pane = pane.clone(); move |toolbar, cx| { - if let Some(workspace) = toolbar - .pane - .upgrade(cx) - .and_then(|pane| pane.read(cx).workspace().upgrade(cx)) + if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx)) { - let pane = pane.clone(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane.clone(), cx).detach_and_log_err(cx); - }); - }) + if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) { + let pane = pane.downgrade(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.go_back(pane, cx).detach_and_log_err(cx); + }); + }) + } } } }, @@ -171,21 +169,17 @@ impl View for Toolbar { enable_go_forward, spacing, { - let pane = pane.clone(); move |toolbar, cx| { - if let Some(workspace) = toolbar - .pane - .upgrade(cx) - .and_then(|pane| pane.read(cx).workspace().upgrade(cx)) + if let Some(pane) = toolbar.pane.as_ref().and_then(|pane| pane.upgrade(cx)) { - let pane = pane.clone(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_forward(pane.clone(), cx) - .detach_and_log_err(cx); - }); - }); + if let Some(workspace) = pane.read(cx).workspace().upgrade(cx) { + let pane = pane.downgrade(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.go_forward(pane, cx).detach_and_log_err(cx); + }); + }) + } } } }, @@ -231,7 +225,7 @@ fn nav_button ) -> AnyElement { MouseEventHandler::::new(0, cx, |state, _| { let style = if enabled { - style.style_for(state, false) + style.style_for(state) } else { style.disabled_style() }; @@ -269,9 +263,9 @@ fn nav_button } impl Toolbar { - pub fn new(pane: WeakViewHandle) -> Self { + pub fn new(pane: Option>) -> Self { Self { - active_pane_item: None, + active_item: None, pane, items: Default::default(), hidden: false, @@ -288,7 +282,7 @@ impl Toolbar { where T: 'static + ToolbarItemView, { - let location = item.set_active_pane_item(self.active_pane_item.as_deref(), cx); + let location = item.set_active_pane_item(self.active_item.as_deref(), cx); cx.subscribe(&item, |this, item, event, cx| { if let Some((_, current_location)) = this.items.iter_mut().find(|(i, _)| i.id() == item.id()) @@ -307,20 +301,16 @@ impl Toolbar { cx.notify(); } - pub fn set_active_pane_item( - &mut self, - pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) { - self.active_pane_item = pane_item.map(|item| item.boxed_clone()); + pub fn set_active_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { + self.active_item = item.map(|item| item.boxed_clone()); self.hidden = self - .active_pane_item + .active_item .as_ref() .map(|item| !item.show_toolbar(cx)) .unwrap_or(false); for (toolbar_item, current_location) in self.items.iter_mut() { - let new_location = toolbar_item.set_active_pane_item(pane_item, cx); + let new_location = toolbar_item.set_active_pane_item(item, cx); if new_location != *current_location { *current_location = new_location; cx.notify(); @@ -328,9 +318,9 @@ impl Toolbar { } } - pub fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut ViewContext) { + pub fn focus_changed(&mut self, focused: bool, cx: &mut ViewContext) { for (toolbar_item, _) in self.items.iter_mut() { - toolbar_item.pane_focus_update(pane_focused, cx); + toolbar_item.focus_changed(focused, cx); } } @@ -364,7 +354,7 @@ impl ToolbarItemViewHandle for ViewHandle { }) } - fn pane_focus_update(&mut self, pane_focused: bool, cx: &mut WindowContext) { + fn focus_changed(&mut self, pane_focused: bool, cx: &mut WindowContext) { self.update(cx, |this, cx| { this.pane_focus_update(pane_focused, cx); cx.notify(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ef8cde78a6..60aefe4213 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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 ModalHandle for ViewHandle { + 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); @@ -140,9 +156,11 @@ pub struct OpenPaths { #[derive(Clone, Deserialize, PartialEq)] pub struct ActivatePane(pub usize); +#[derive(Deserialize)] pub struct Toast { id: usize, msg: Cow<'static, str>, + #[serde(skip)] on_click: Option<(Cow<'static, str>, Arc)>, } @@ -183,9 +201,9 @@ impl Clone for Toast { } } -pub type WorkspaceId = i64; +impl_actions!(workspace, [ActivatePane, Toast]); -impl_actions!(workspace, [ActivatePane]); +pub type WorkspaceId = i64; pub fn init_settings(cx: &mut AppContext) { settings::register::(cx); @@ -464,7 +482,7 @@ pub enum Event { pub struct Workspace { weak_self: WeakViewHandle, remote_entity_subscription: Option, - modal: Option, + modal: Option, zoomed: Option, zoomed_position: Option, center: PaneGroup, @@ -493,6 +511,11 @@ pub struct Workspace { pane_history_timestamp: Arc, } +struct ActiveModal { + view: Box, + previously_focused_view_id: Option, +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct ViewId { pub creator: PeerId, @@ -553,6 +576,10 @@ impl Workspace { } } + project::Event::Notification(message) => this.show_notification(0, cx, |cx| { + cx.add_view(|_| MessageNotification::new(message.clone())) + }), + _ => {} } cx.notify() @@ -855,7 +882,10 @@ impl Workspace { &self.right_dock } - pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) + where + T::Event: std::fmt::Debug, + { let dock = match panel.position(cx) { DockPosition::Left => &self.left_dock, DockPosition::Bottom => &self.bottom_dock, @@ -898,10 +928,11 @@ impl Workspace { }); } else if T::should_zoom_in_on_event(event) { dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); - if panel.has_focus(cx) { - this.zoomed = Some(panel.downgrade().into_any()); - this.zoomed_position = Some(panel.read(cx).position(cx)); + if !panel.has_focus(cx) { + cx.focus(&panel); } + this.zoomed = Some(panel.downgrade().into_any()); + this.zoomed_position = Some(panel.read(cx).position(cx)); } else if T::should_zoom_out_on_event(event) { dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); if this.zoomed_position == Some(prev_position) { @@ -919,6 +950,7 @@ impl Workspace { this.zoomed = None; this.zoomed_position = None; } + this.update_active_view_for_followers(cx); cx.notify(); } } @@ -1471,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::()); - if let Some(already_open_modal) = already_open_modal { + if let Some(already_open_modal) = self + .dismiss_modal(cx) + .and_then(|modal| modal.downcast::()) + { cx.focus_self(); Some(already_open_modal) } else { @@ -1483,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 } } @@ -1492,13 +1530,20 @@ impl Workspace { pub fn modal(&self) -> Option> { self.modal .as_ref() - .and_then(|modal| modal.clone().downcast::()) + .and_then(|modal| modal.view.as_any().clone().downcast::()) } - pub fn dismiss_modal(&mut self, cx: &mut ViewContext) { - if self.modal.take().is_some() { - cx.focus(&self.active_pane); + pub fn dismiss_modal(&mut self, cx: &mut ViewContext) -> Option { + 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 } } @@ -1598,9 +1643,7 @@ impl Workspace { focus_center = true; } } else { - if active_panel.is_zoomed(cx) { - cx.focus(active_panel.as_any()); - } + cx.focus(active_panel.as_any()); reveal_dock = true; } } @@ -1697,6 +1740,11 @@ impl Workspace { cx.notify(); } + #[cfg(any(test, feature = "test-support"))] + pub fn zoomed_view(&self, cx: &AppContext) -> Option { + self.zoomed.and_then(|view| view.upgrade(cx)) + } + fn dismiss_zoomed_items_to_reveal( &mut self, dock_to_reveal: Option, @@ -1946,18 +1994,7 @@ impl Workspace { self.zoomed = None; } self.zoomed_position = None; - - self.update_followers( - proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { - id: self.active_item(cx).and_then(|item| { - item.to_followable_item_handle(cx)? - .remote_id(&self.app_state.client, cx) - .map(|id| id.to_proto()) - }), - leader_id: self.leader_for_pane(&pane), - }), - cx, - ); + self.update_active_view_for_followers(cx); cx.notify(); } @@ -2293,11 +2330,11 @@ impl Workspace { // (https://github.com/zed-industries/zed/issues/1290) let is_fullscreen = cx.window_is_fullscreen(); let container_theme = if is_fullscreen { - let mut container_theme = theme.workspace.titlebar.container; + let mut container_theme = theme.titlebar.container; container_theme.padding.left = container_theme.padding.right; container_theme } else { - theme.workspace.titlebar.container + theme.titlebar.container }; enum TitleBar {} @@ -2317,7 +2354,7 @@ impl Workspace { } }) .constrained() - .with_height(theme.workspace.titlebar.height) + .with_height(theme.titlebar.height) .into_any_named("titlebar") } @@ -2646,6 +2683,30 @@ impl Workspace { Ok(()) } + fn update_active_view_for_followers(&self, cx: &AppContext) { + if self.active_pane.read(cx).has_focus() { + self.update_followers( + proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { + id: self.active_item(cx).and_then(|item| { + item.to_followable_item_handle(cx)? + .remote_id(&self.app_state.client, cx) + .map(|id| id.to_proto()) + }), + leader_id: self.leader_for_pane(&self.active_pane), + }), + cx, + ); + } else { + self.update_followers( + proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { + id: None, + leader_id: None, + }), + cx, + ); + } + } + fn update_followers( &self, update: proto::update_followers::Variant, @@ -2693,12 +2754,10 @@ impl Workspace { .and_then(|id| state.items_by_leader_view_id.get(&id)) { items_to_activate.push((pane.clone(), item.boxed_clone())); - } else { - if let Some(shared_screen) = - self.shared_screen_for_peer(leader_id, pane, cx) - { - items_to_activate.push((pane.clone(), Box::new(shared_screen))); - } + } else if let Some(shared_screen) = + self.shared_screen_for_peer(leader_id, pane, cx) + { + items_to_activate.push((pane.clone(), Box::new(shared_screen))); } } } @@ -2740,7 +2799,7 @@ impl Workspace { let call = self.active_call()?; let room = call.read(cx).room()?.read(cx); let participant = room.remote_participant_for_peer_id(peer_id)?; - let track = participant.tracks.values().next()?.clone(); + let track = participant.video_tracks.values().next()?.clone(); let user = participant.user.clone(); for item in pane.read(cx).items_of_type::() { @@ -2838,7 +2897,7 @@ impl Workspace { cx.notify(); } - fn serialize_workspace(&self, cx: &AppContext) { + fn serialize_workspace(&self, cx: &ViewContext) { fn serialize_pane_handle( pane_handle: &ViewHandle, cx: &AppContext, @@ -2881,7 +2940,7 @@ impl Workspace { } } - fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure { + fn build_serialized_docks(this: &Workspace, cx: &ViewContext) -> DockStructure { let left_dock = this.left_dock.read(cx); let left_visible = left_dock.is_open(); let left_active_panel = left_dock.visible_panel().and_then(|panel| { @@ -2890,6 +2949,10 @@ impl Workspace { .to_string(), ) }); + let left_dock_zoom = left_dock + .visible_panel() + .map(|panel| panel.is_zoomed(cx)) + .unwrap_or(false); let right_dock = this.right_dock.read(cx); let right_visible = right_dock.is_open(); @@ -2899,6 +2962,10 @@ impl Workspace { .to_string(), ) }); + let right_dock_zoom = right_dock + .visible_panel() + .map(|panel| panel.is_zoomed(cx)) + .unwrap_or(false); let bottom_dock = this.bottom_dock.read(cx); let bottom_visible = bottom_dock.is_open(); @@ -2908,19 +2975,26 @@ impl Workspace { .to_string(), ) }); + let bottom_dock_zoom = bottom_dock + .visible_panel() + .map(|panel| panel.is_zoomed(cx)) + .unwrap_or(false); DockStructure { left: DockData { visible: left_visible, active_panel: left_active_panel, + zoom: left_dock_zoom, }, right: DockData { visible: right_visible, active_panel: right_active_panel, + zoom: right_dock_zoom, }, bottom: DockData { visible: bottom_visible, active_panel: bottom_active_panel, + zoom: bottom_dock_zoom, }, } } @@ -3033,14 +3107,31 @@ impl Workspace { dock.activate_panel(ix, cx); } } + dock.active_panel() + .map(|panel| { + panel.set_zoomed(docks.left.zoom, cx) + }); + if docks.left.visible && docks.left.zoom { + cx.focus_self() + } }); + // TODO: I think the bug is that setting zoom or active undoes the bottom zoom or something workspace.right_dock.update(cx, |dock, cx| { dock.set_open(docks.right.visible, cx); if let Some(active_panel) = docks.right.active_panel { if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { dock.activate_panel(ix, cx); + } } + dock.active_panel() + .map(|panel| { + panel.set_zoomed(docks.right.zoom, cx) + }); + + if docks.right.visible && docks.right.zoom { + cx.focus_self() + } }); workspace.bottom_dock.update(cx, |dock, cx| { dock.set_open(docks.bottom.visible, cx); @@ -3049,8 +3140,18 @@ impl Workspace { dock.activate_panel(ix, cx); } } + + dock.active_panel() + .map(|panel| { + panel.set_zoomed(docks.bottom.zoom, cx) + }); + + if docks.bottom.visible && docks.bottom.zoom { + cx.focus_self() + } }); + cx.notify(); })?; @@ -3429,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() @@ -4413,7 +4514,7 @@ mod tests { workspace.read_with(cx, |workspace, cx| { assert!(workspace.right_dock().read(cx).is_open()); assert!(!panel.is_zoomed(cx)); - assert!(!panel.has_focus(cx)); + assert!(panel.has_focus(cx)); }); // Focus and zoom panel @@ -4488,7 +4589,7 @@ mod tests { workspace.read_with(cx, |workspace, cx| { let pane = pane.read(cx); assert!(!pane.is_zoomed()); - assert!(pane.has_focus()); + assert!(!pane.has_focus()); assert!(workspace.right_dock().read(cx).is_open()); assert!(workspace.zoomed.is_none()); }); diff --git a/crates/xtask/Cargo.toml b/crates/xtask/Cargo.toml new file mode 100644 index 0000000000..3d3f7d4357 --- /dev/null +++ b/crates/xtask/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" +publish = false +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +clap = {version = "4.0", features = ["derive"]} +theme = {path = "../theme"} +serde_json.workspace = true +schemars.workspace = true diff --git a/crates/xtask/src/cli.rs b/crates/xtask/src/cli.rs new file mode 100644 index 0000000000..bffda1bc16 --- /dev/null +++ b/crates/xtask/src/cli.rs @@ -0,0 +1,23 @@ +use clap::{Parser, Subcommand}; +use std::path::PathBuf; +/// Common utilities for Zed developers. +// For more information, see [matklad's repository README](https://github.com/matklad/cargo-xtask/) +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] +pub struct Cli { + #[command(subcommand)] + pub command: Commands, +} + +/// Command to run. +#[derive(Subcommand)] +pub enum Commands { + /// Builds theme types for interop with Typescript. + BuildThemeTypes { + #[clap(short, long, default_value = "schemas")] + out_dir: PathBuf, + #[clap(short, long, default_value = "theme.json")] + file_name: PathBuf, + }, +} diff --git a/crates/xtask/src/main.rs b/crates/xtask/src/main.rs new file mode 100644 index 0000000000..38e5658ce7 --- /dev/null +++ b/crates/xtask/src/main.rs @@ -0,0 +1,29 @@ +mod cli; + +use std::path::PathBuf; + +use anyhow::Result; +use clap::Parser; +use schemars::schema_for; +use theme::Theme; + +fn build_themes(out_dir: PathBuf, file_name: PathBuf) -> Result<()> { + let theme = schema_for!(Theme); + let output = serde_json::to_string_pretty(&theme)?; + + std::fs::create_dir(&out_dir)?; + + let mut file_path = out_dir; + file_path.push(file_name); + + std::fs::write(file_path, output)?; + + Ok(()) +} + +fn main() -> Result<()> { + let args = cli::Cli::parse(); + match args.command { + cli::Commands::BuildThemeTypes { out_dir, file_name } => build_themes(out_dir, file_name), + } +} diff --git a/crates/zed-actions/Cargo.toml b/crates/zed-actions/Cargo.toml new file mode 100644 index 0000000000..b3fe3cbb53 --- /dev/null +++ b/crates/zed-actions/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "zed-actions" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +gpui = { path = "../gpui" } diff --git a/crates/zed-actions/src/lib.rs b/crates/zed-actions/src/lib.rs new file mode 100644 index 0000000000..bcd086924d --- /dev/null +++ b/crates/zed-actions/src/lib.rs @@ -0,0 +1,28 @@ +use gpui::actions; + +actions!( + zed, + [ + About, + Hide, + HideOthers, + ShowAll, + Minimize, + Zoom, + ToggleFullScreen, + Quit, + DebugElements, + OpenLog, + OpenLicenses, + OpenTelemetryLog, + OpenKeymap, + OpenSettings, + OpenLocalSettings, + OpenDefaultSettings, + OpenDefaultKeymap, + IncreaseBufferFontSize, + DecreaseBufferFontSize, + ResetBufferFontSize, + ResetDatabase, + ] +); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index d8e47d1c3e..d016525af4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.92.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" } @@ -62,12 +63,11 @@ text = { path = "../text" } terminal_view = { path = "../terminal_view" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } -theme_testbench = { path = "../theme_testbench" } util = { path = "../util" } vim = { path = "../vim" } workspace = { path = "../workspace" } welcome = { path = "../welcome" } - +zed-actions = {path = "../zed-actions"} anyhow.workspace = true async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-tar = "0.4.2" diff --git a/crates/zed/src/assets.rs b/crates/zed/src/assets.rs index 6eb8a44f0f..574016c25d 100644 --- a/crates/zed/src/assets.rs +++ b/crates/zed/src/assets.rs @@ -7,6 +7,7 @@ use rust_embed::RustEmbed; #[include = "fonts/**/*"] #[include = "icons/**/*"] #[include = "themes/**/*"] +#[include = "sounds/**/*"] #[include = "*.md"] #[exclude = "*.DS_Store"] pub struct Assets; diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 7e4ddcef19..47aa2b739c 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -2,14 +2,14 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; pub use language::*; +use lsp::LanguageServerBinary; use smol::fs::{self, File}; use std::{any::Any, path::PathBuf, sync::Arc}; -use util::fs::remove_matching; -use util::github::latest_github_release; -use util::http::HttpClient; -use util::ResultExt; - -use util::github::GitHubLspBinaryVersion; +use util::{ + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; pub struct CLspAdapter; @@ -21,9 +21,9 @@ impl super::LspAdapter for CLspAdapter { async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result> { - let release = latest_github_release("clangd/clangd", false, http).await?; + let release = latest_github_release("clangd/clangd", false, delegate.http_client()).await?; let asset_name = format!("clangd-mac-{}.zip", release.name); let asset = release .assets @@ -40,8 +40,8 @@ impl super::LspAdapter for CLspAdapter { async fn fetch_server_binary( &self, version: Box, - http: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); @@ -49,7 +49,8 @@ impl super::LspAdapter for CLspAdapter { let binary_path = version_dir.join("bin/clangd"); if fs::metadata(&binary_path).await.is_err() { - let mut response = http + let mut response = delegate + .http_client() .get(&version.url, Default::default(), true) .await .context("error downloading release")?; @@ -81,32 +82,24 @@ impl super::LspAdapter for CLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last_clangd_dir = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_clangd_dir = Some(entry.path()); - } - } - let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let clangd_bin = clangd_dir.join("bin/clangd"); - if clangd_bin.exists() { - Ok(LanguageServerBinary { - path: clangd_bin, - arguments: vec![], - }) - } else { - Err(anyhow!( - "missing clangd binary in directory {:?}", - clangd_dir - )) - } - })() - .await - .log_err() + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) } async fn label_for_completion( @@ -246,6 +239,34 @@ impl super::LspAdapter for CLspAdapter { } } +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last_clangd_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_clangd_dir = Some(entry.path()); + } + } + let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let clangd_bin = clangd_dir.join("bin/clangd"); + if clangd_bin.exists() { + Ok(LanguageServerBinary { + path: clangd_bin, + arguments: vec![], + }) + } else { + Err(anyhow!( + "missing clangd binary in directory {:?}", + clangd_dir + )) + } + })() + .await + .log_err() +} + #[cfg(test)] mod tests { use gpui::TestAppContext; diff --git a/crates/zed/src/languages/elixir.rs b/crates/zed/src/languages/elixir.rs index 2939a0fa5f..c32927e15c 100644 --- a/crates/zed/src/languages/elixir.rs +++ b/crates/zed/src/languages/elixir.rs @@ -1,16 +1,23 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use futures::StreamExt; +use gpui::{AsyncAppContext, Task}; pub use language::*; -use lsp::{CompletionItemKind, SymbolKind}; +use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind}; use smol::fs::{self, File}; -use std::{any::Any, path::PathBuf, sync::Arc}; -use util::fs::remove_matching; -use util::github::latest_github_release; -use util::http::HttpClient; -use util::ResultExt; - -use util::github::GitHubLspBinaryVersion; +use std::{ + any::Any, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, +}; +use util::{ + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; pub struct ElixirLspAdapter; @@ -20,19 +27,58 @@ impl LspAdapter for ElixirLspAdapter { LanguageServerName("elixir-ls".into()) } + fn will_start_server( + &self, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Option>> { + static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false); + + const NOTIFICATION_MESSAGE: &str = "Could not run the elixir language server, `elixir-ls`, because `elixir` was not found."; + + let delegate = delegate.clone(); + Some(cx.spawn(|mut cx| async move { + let elixir_output = smol::process::Command::new("elixir") + .args(["--version"]) + .output() + .await; + if elixir_output.is_err() { + if DID_SHOW_NOTIFICATION + .compare_exchange(false, true, SeqCst, SeqCst) + .is_ok() + { + cx.update(|cx| { + delegate.show_notification(NOTIFICATION_MESSAGE, cx); + }) + } + return Err(anyhow!("cannot run elixir-ls")); + } + + Ok(()) + })) + } + async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result> { + let http = delegate.http_client(); let release = latest_github_release("elixir-lsp/elixir-ls", false, http).await?; - let asset_name = "elixir-ls.zip"; + let version_name = release + .name + .strip_prefix("Release ") + .context("Elixir-ls release name does not start with prefix")? + .to_owned(); + + let asset_name = format!("elixir-ls-{}.zip", &version_name); let asset = release .assets .iter() .find(|asset| asset.name == asset_name) .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + let version = GitHubLspBinaryVersion { - name: release.name, + name: version_name, url: asset.browser_download_url.clone(), }; Ok(Box::new(version) as Box<_>) @@ -41,8 +87,8 @@ impl LspAdapter for ElixirLspAdapter { async fn fetch_server_binary( &self, version: Box, - http: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); let zip_path = container_dir.join(format!("elixir-ls_{}.zip", version.name)); @@ -50,7 +96,8 @@ impl LspAdapter for ElixirLspAdapter { let binary_path = version_dir.join("language_server.sh"); if fs::metadata(&binary_path).await.is_err() { - let mut response = http + let mut response = delegate + .http_client() .get(&version.url, Default::default(), true) .await .context("error downloading release")?; @@ -76,7 +123,7 @@ impl LspAdapter for ElixirLspAdapter { .await? .status; if !unzip_status.success() { - Err(anyhow!("failed to unzip clangd archive"))?; + Err(anyhow!("failed to unzip elixir-ls archive"))?; } remove_matching(&container_dir, |entry| entry != version_dir).await; @@ -88,21 +135,19 @@ impl LspAdapter for ElixirLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - last = Some(entry?.path()); - } - last.map(|path| LanguageServerBinary { - path, - arguments: vec![], - }) - .ok_or_else(|| anyhow!("no cached binary")) - })() - .await - .log_err() + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir).await } async fn label_for_completion( @@ -188,3 +233,20 @@ impl LspAdapter for ElixirLspAdapter { }) } } + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + last.map(|path| LanguageServerBinary { + path, + arguments: vec![], + }) + .ok_or_else(|| anyhow!("no cached binary")) + })() + .await + .log_err() +} diff --git a/crates/zed/src/languages/elixir/highlights.scm b/crates/zed/src/languages/elixir/highlights.scm index deea51c436..0e779d195c 100644 --- a/crates/zed/src/languages/elixir/highlights.scm +++ b/crates/zed/src/languages/elixir/highlights.scm @@ -36,8 +36,6 @@ (char) @constant -(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded - (escape_sequence) @string.escape [ @@ -146,3 +144,10 @@ "<<" ">>" ] @punctuation.bracket + +(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded + +((sigil + (sigil_name) @_sigil_name + (quoted_content) @embedded) + (#eq? @_sigil_name "H")) diff --git a/crates/zed/src/languages/go.rs b/crates/zed/src/languages/go.rs index ed24abb45c..d7982f7bdb 100644 --- a/crates/zed/src/languages/go.rs +++ b/crates/zed/src/languages/go.rs @@ -1,16 +1,24 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; +use gpui::{AsyncAppContext, Task}; pub use language::*; use lazy_static::lazy_static; +use lsp::LanguageServerBinary; use regex::Regex; use smol::{fs, process}; -use std::ffi::{OsStr, OsString}; -use std::{any::Any, ops::Range, path::PathBuf, str, sync::Arc}; -use util::fs::remove_matching; -use util::github::latest_github_release; -use util::http::HttpClient; -use util::ResultExt; +use std::{ + any::Any, + ffi::{OsStr, OsString}, + ops::Range, + path::PathBuf, + str, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, +}; +use util::{fs::remove_matching, github::latest_github_release, ResultExt}; fn server_binary_arguments() -> Vec { vec!["-mode=stdio".into()] @@ -31,9 +39,9 @@ impl super::LspAdapter for GoLspAdapter { async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result> { - let release = latest_github_release("golang/tools", false, http).await?; + let release = latest_github_release("golang/tools", false, delegate.http_client()).await?; let version: Option = release.name.strip_prefix("gopls/v").map(str::to_string); if version.is_none() { log::warn!( @@ -44,11 +52,39 @@ impl super::LspAdapter for GoLspAdapter { Ok(Box::new(version) as Box<_>) } + fn will_fetch_server( + &self, + delegate: &Arc, + cx: &mut AsyncAppContext, + ) -> Option>> { + static DID_SHOW_NOTIFICATION: AtomicBool = AtomicBool::new(false); + + const NOTIFICATION_MESSAGE: &str = + "Could not install the Go language server `gopls`, because `go` was not found."; + + let delegate = delegate.clone(); + Some(cx.spawn(|mut cx| async move { + let install_output = process::Command::new("go").args(["version"]).output().await; + if install_output.is_err() { + if DID_SHOW_NOTIFICATION + .compare_exchange(false, true, SeqCst, SeqCst) + .is_ok() + { + cx.update(|cx| { + delegate.show_notification(NOTIFICATION_MESSAGE, cx); + }) + } + return Err(anyhow!("cannot install gopls")); + } + Ok(()) + })) + } + async fn fetch_server_binary( &self, version: Box, - _: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::>().unwrap(); let this = *self; @@ -68,7 +104,10 @@ impl super::LspAdapter for GoLspAdapter { }); } } - } else if let Some(path) = this.cached_server_binary(container_dir.clone()).await { + } else if let Some(path) = this + .cached_server_binary(container_dir.clone(), delegate) + .await + { return Ok(path); } @@ -105,33 +144,24 @@ impl super::LspAdapter for GoLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last_binary_path = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_file() - && entry - .file_name() - .to_str() - .map_or(false, |name| name.starts_with("gopls_")) - { - last_binary_path = Some(entry.path()); - } - } + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } - if let Some(path) = last_binary_path { - Ok(LanguageServerBinary { - path, - arguments: server_binary_arguments(), - }) - } else { - Err(anyhow!("no cached binary")) - } - })() - .await - .log_err() + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary + }) } async fn label_for_completion( @@ -294,6 +324,35 @@ impl super::LspAdapter for GoLspAdapter { } } +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name.starts_with("gopls_")) + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: server_binary_arguments(), + }) + } else { + Err(anyhow!("no cached binary")) + } + })() + .await + .log_err() +} + fn adjust_runs( delta: usize, mut runs: Vec<(Range, HighlightId)>, diff --git a/crates/zed/src/languages/heex/highlights.scm b/crates/zed/src/languages/heex/highlights.scm index fa88acd4d9..8728110d58 100644 --- a/crates/zed/src/languages/heex/highlights.scm +++ b/crates/zed/src/languages/heex/highlights.scm @@ -1,17 +1,11 @@ ; HEEx delimiters [ - "%>" "--%>" "-->" "/>" "" +] @keyword + ; HEEx operators are highlighted as such "=" @operator diff --git a/crates/zed/src/languages/heex/injections.scm b/crates/zed/src/languages/heex/injections.scm index 0d4977b28a..b503bcb28d 100644 --- a/crates/zed/src/languages/heex/injections.scm +++ b/crates/zed/src/languages/heex/injections.scm @@ -1,13 +1,13 @@ -((directive (partial_expression_value) @content) - (#set! language "elixir") - (#set! include-children) - (#set! combined)) +( + (directive + [ + (partial_expression_value) + (expression_value) + (ending_expression_value) + ] @content) + (#set! language "elixir") + (#set! combined) +) -; Regular expression_values do not need to be combined -((directive (expression_value) @content) - (#set! language "elixir")) - -; expressions live within HTML tags, and do not need to be combined -; ((expression (expression_value) @content) (#set! language "elixir")) diff --git a/crates/zed/src/languages/html.rs b/crates/zed/src/languages/html.rs index 68f780c3af..ecc839fca6 100644 --- a/crates/zed/src/languages/html.rs +++ b/crates/zed/src/languages/html.rs @@ -1,16 +1,22 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; use smol::fs; -use std::ffi::OsString; -use std::path::Path; -use std::{any::Any, path::PathBuf, sync::Arc}; -use util::http::HttpClient; +use std::{ + any::Any, + ffi::OsString, + path::{Path, PathBuf}, + sync::Arc, +}; use util::ResultExt; +const SERVER_PATH: &'static str = + "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server"; + fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] } @@ -20,9 +26,6 @@ pub struct HtmlLspAdapter { } impl HtmlLspAdapter { - const SERVER_PATH: &'static str = - "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server"; - pub fn new(node: Arc) -> Self { HtmlLspAdapter { node } } @@ -36,7 +39,7 @@ impl LspAdapter for HtmlLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { Ok(Box::new( self.node @@ -48,11 +51,11 @@ impl LspAdapter for HtmlLspAdapter { async fn fetch_server_binary( &self, version: Box, - _: Arc, container_dir: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); - let server_path = container_dir.join(Self::SERVER_PATH); + let server_path = container_dir.join(SERVER_PATH); if fs::metadata(&server_path).await.is_err() { self.node @@ -69,32 +72,19 @@ impl LspAdapter for HtmlLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last_version_dir = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_version_dir = Some(entry.path()); - } - } - let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let server_path = last_version_dir.join(Self::SERVER_PATH); - if server_path.exists() { - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - arguments: server_binary_arguments(&server_path), - }) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - })() - .await - .log_err() + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await } async fn initialization_options(&self) -> Option { @@ -103,3 +93,34 @@ impl LspAdapter for HtmlLspAdapter { })) } } + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index e1f3da9e02..b7e4ab4ba7 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use collections::HashMap; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; -use language::{LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter}; +use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::json; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; @@ -16,7 +17,6 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::http::HttpClient; use util::{paths, ResultExt}; const SERVER_PATH: &'static str = @@ -45,7 +45,7 @@ impl LspAdapter for JsonLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { Ok(Box::new( self.node @@ -57,8 +57,8 @@ impl LspAdapter for JsonLspAdapter { async fn fetch_server_binary( &self, version: Box, - _: Arc, container_dir: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); let server_path = container_dir.join(SERVER_PATH); @@ -78,33 +78,19 @@ impl LspAdapter for JsonLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last_version_dir = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_version_dir = Some(entry.path()); - } - } + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } - let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let server_path = last_version_dir.join(SERVER_PATH); - if server_path.exists() { - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - arguments: server_binary_arguments(&server_path), - }) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - })() - .await - .log_err() + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await } async fn initialization_options(&self) -> Option { @@ -157,6 +143,38 @@ impl LspAdapter for JsonLspAdapter { } } +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} + fn schema_file_match(path: &Path) -> &Path { path.strip_prefix(path.parent().unwrap().parent().unwrap()) .unwrap() diff --git a/crates/zed/src/languages/language_plugin.rs b/crates/zed/src/languages/language_plugin.rs index 9b82713d08..b071936392 100644 --- a/crates/zed/src/languages/language_plugin.rs +++ b/crates/zed/src/languages/language_plugin.rs @@ -3,10 +3,10 @@ use async_trait::async_trait; use collections::HashMap; use futures::lock::Mutex; use gpui::executor::Background; -use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn}; use std::{any::Any, path::PathBuf, sync::Arc}; -use util::http::HttpClient; use util::ResultExt; #[allow(dead_code)] @@ -72,7 +72,7 @@ impl LspAdapter for PluginLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { let runtime = self.runtime.clone(); let function = self.fetch_latest_server_version; @@ -92,8 +92,8 @@ impl LspAdapter for PluginLspAdapter { async fn fetch_server_binary( &self, version: Box, - _: Arc, container_dir: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { let version = *version.downcast::().unwrap(); let runtime = self.runtime.clone(); @@ -110,7 +110,11 @@ impl LspAdapter for PluginLspAdapter { .await } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { let runtime = self.runtime.clone(); let function = self.cached_server_binary; @@ -126,6 +130,14 @@ impl LspAdapter for PluginLspAdapter { .await } + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } + async fn initialization_options(&self) -> Option { let string: String = self .runtime diff --git a/crates/zed/src/languages/lua.rs b/crates/zed/src/languages/lua.rs index f204eb2555..7c5c7179d0 100644 --- a/crates/zed/src/languages/lua.rs +++ b/crates/zed/src/languages/lua.rs @@ -3,12 +3,15 @@ use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; use async_trait::async_trait; use futures::{io::BufReader, StreamExt}; -use language::{LanguageServerBinary, LanguageServerName}; +use language::{LanguageServerName, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use smol::fs; -use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc}; -use util::{async_iife, github::latest_github_release, http::HttpClient, ResultExt}; - -use util::github::GitHubLspBinaryVersion; +use std::{any::Any, env::consts, ffi::OsString, path::PathBuf}; +use util::{ + async_iife, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; #[derive(Copy, Clone)] pub struct LuaLspAdapter; @@ -28,9 +31,11 @@ impl super::LspAdapter for LuaLspAdapter { async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result> { - let release = latest_github_release("LuaLS/lua-language-server", false, http).await?; + let release = + latest_github_release("LuaLS/lua-language-server", false, delegate.http_client()) + .await?; let version = release.name.clone(); let platform = match consts::ARCH { "x86_64" => "x64", @@ -53,15 +58,16 @@ impl super::LspAdapter for LuaLspAdapter { async fn fetch_server_binary( &self, version: Box, - http: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); let binary_path = container_dir.join("bin/lua-language-server"); if fs::metadata(&binary_path).await.is_err() { - let mut response = http + let mut response = delegate + .http_client() .get(&version.url, Default::default(), true) .await .map_err(|err| anyhow!("error downloading release: {}", err))?; @@ -81,32 +87,52 @@ impl super::LspAdapter for LuaLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - async_iife!({ - let mut last_binary_path = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_file() - && entry - .file_name() - .to_str() - .map_or(false, |name| name == "lua-language-server") - { - last_binary_path = Some(entry.path()); - } - } + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } - if let Some(path) = last_binary_path { - Ok(LanguageServerBinary { - path, - arguments: server_binary_arguments(), - }) - } else { - Err(anyhow!("no cached binary")) - } - }) - .await - .log_err() + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--version".into()]; + binary + }) } } + +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + async_iife!({ + let mut last_binary_path = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_file() + && entry + .file_name() + .to_str() + .map_or(false, |name| name == "lua-language-server") + { + last_binary_path = Some(entry.path()); + } + } + + if let Some(path) = last_binary_path { + Ok(LanguageServerBinary { + path, + arguments: server_binary_arguments(), + }) + } else { + Err(anyhow!("no cached binary")) + } + }) + .await + .log_err() +} diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 7aaddf5fe8..41ad28ba86 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -1,7 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use futures::StreamExt; -use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use smol::fs; use std::{ @@ -10,9 +11,10 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::http::HttpClient; use util::ResultExt; +const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js"; + fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] } @@ -22,8 +24,6 @@ pub struct PythonLspAdapter { } impl PythonLspAdapter { - const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js"; - pub fn new(node: Arc) -> Self { PythonLspAdapter { node } } @@ -37,7 +37,7 @@ impl LspAdapter for PythonLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>) } @@ -45,11 +45,11 @@ impl LspAdapter for PythonLspAdapter { async fn fetch_server_binary( &self, version: Box, - _: Arc, container_dir: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); - let server_path = container_dir.join(Self::SERVER_PATH); + let server_path = container_dir.join(SERVER_PATH); if fs::metadata(&server_path).await.is_err() { self.node @@ -63,32 +63,19 @@ impl LspAdapter for PythonLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last_version_dir = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_version_dir = Some(entry.path()); - } - } - let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let server_path = last_version_dir.join(Self::SERVER_PATH); - if server_path.exists() { - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - arguments: server_binary_arguments(&server_path), - }) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - })() - .await - .log_err() + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await } async fn process_completion(&self, item: &mut lsp::CompletionItem) { @@ -167,6 +154,37 @@ impl LspAdapter for PythonLspAdapter { } } +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} + #[cfg(test)] mod tests { use gpui::{ModelContext, TestAppContext}; diff --git a/crates/zed/src/languages/ruby.rs b/crates/zed/src/languages/ruby.rs index d387f815f0..358441352a 100644 --- a/crates/zed/src/languages/ruby.rs +++ b/crates/zed/src/languages/ruby.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::LanguageServerBinary; use std::{any::Any, path::PathBuf, sync::Arc}; -use util::http::HttpClient; pub struct RubyLanguageServer; @@ -14,7 +14,7 @@ impl LspAdapter for RubyLanguageServer { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { Ok(Box::new(())) } @@ -22,19 +22,31 @@ impl LspAdapter for RubyLanguageServer { async fn fetch_server_binary( &self, _version: Box, - _: Arc, _container_dir: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { Err(anyhow!("solargraph must be installed manually")) } - async fn cached_server_binary(&self, _container_dir: PathBuf) -> Option { + async fn cached_server_binary( + &self, + _: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { Some(LanguageServerBinary { path: "solargraph".into(), arguments: vec!["stdio".into()], }) } + fn can_be_reinstalled(&self) -> bool { + false + } + + async fn installation_test_binary(&self, _: PathBuf) -> Option { + None + } + async fn label_for_completion( &self, item: &lsp::CompletionItem, diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 15700ec80a..97549b0058 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -4,13 +4,15 @@ use async_trait::async_trait; use futures::{io::BufReader, StreamExt}; pub use language::*; use lazy_static::lazy_static; +use lsp::LanguageServerBinary; use regex::Regex; use smol::fs::{self, File}; use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; -use util::fs::remove_matching; -use util::github::{latest_github_release, GitHubLspBinaryVersion}; -use util::http::HttpClient; -use util::ResultExt; +use util::{ + fs::remove_matching, + github::{latest_github_release, GitHubLspBinaryVersion}, + ResultExt, +}; pub struct RustLspAdapter; @@ -22,9 +24,11 @@ impl LspAdapter for RustLspAdapter { async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result> { - let release = latest_github_release("rust-analyzer/rust-analyzer", false, http).await?; + let release = + latest_github_release("rust-analyzer/rust-analyzer", false, delegate.http_client()) + .await?; let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); let asset = release .assets @@ -40,14 +44,15 @@ impl LspAdapter for RustLspAdapter { async fn fetch_server_binary( &self, version: Box, - http: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); if fs::metadata(&destination_path).await.is_err() { - let mut response = http + let mut response = delegate + .http_client() .get(&version.url, Default::default(), true) .await .map_err(|err| anyhow!("error downloading release: {}", err))?; @@ -69,21 +74,24 @@ impl LspAdapter for RustLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - last = Some(entry?.path()); - } + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir).await + } - anyhow::Ok(LanguageServerBinary { - path: last.ok_or_else(|| anyhow!("no cached binary"))?, - arguments: Default::default(), + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir) + .await + .map(|mut binary| { + binary.arguments = vec!["--help".into()]; + binary }) - })() - .await - .log_err() } async fn disk_based_diagnostic_sources(&self) -> Vec { @@ -250,6 +258,22 @@ impl LspAdapter for RustLspAdapter { }) } } +async fn get_cached_server_binary(container_dir: PathBuf) -> Option { + (|| async move { + let mut last = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + last = Some(entry?.path()); + } + + anyhow::Ok(LanguageServerBinary { + path: last.ok_or_else(|| anyhow!("no cached binary"))?, + arguments: Default::default(), + }) + })() + .await + .log_err() +} #[cfg(test)] mod tests { diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 7d2d580857..0a47d365b5 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -4,8 +4,8 @@ use async_tar::Archive; use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt}; use gpui::AppContext; -use language::{LanguageServerBinary, LanguageServerName, LspAdapter}; -use lsp::CodeActionKind; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::{CodeActionKind, LanguageServerBinary}; use node_runtime::NodeRuntime; use serde_json::{json, Value}; use smol::{fs, io::BufReader, stream::StreamExt}; @@ -16,7 +16,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::{fs::remove_matching, github::latest_github_release, http::HttpClient}; +use util::{fs::remove_matching, github::latest_github_release}; use util::{github::GitHubLspBinaryVersion, ResultExt}; fn typescript_server_binary_arguments(server_path: &Path) -> Vec { @@ -58,7 +58,7 @@ impl LspAdapter for TypeScriptLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { Ok(Box::new(TypeScriptVersions { typescript_version: self.node.npm_package_latest_version("typescript").await?, @@ -72,8 +72,8 @@ impl LspAdapter for TypeScriptLspAdapter { async fn fetch_server_binary( &self, version: Box, - _: Arc, container_dir: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); let server_path = container_dir.join(Self::NEW_SERVER_PATH); @@ -99,29 +99,19 @@ impl LspAdapter for TypeScriptLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let old_server_path = container_dir.join(Self::OLD_SERVER_PATH); - let new_server_path = container_dir.join(Self::NEW_SERVER_PATH); - if new_server_path.exists() { - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - arguments: typescript_server_binary_arguments(&new_server_path), - }) - } else if old_server_path.exists() { - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - arguments: typescript_server_binary_arguments(&old_server_path), - }) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - container_dir - )) - } - })() - .await - .log_err() + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_ts_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_ts_server_binary(container_dir, &self.node).await } fn code_action_kinds(&self) -> Option> { @@ -169,6 +159,34 @@ impl LspAdapter for TypeScriptLspAdapter { } } +async fn get_cached_ts_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH); + let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH); + if new_server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: typescript_server_binary_arguments(&new_server_path), + }) + } else if old_server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: typescript_server_binary_arguments(&old_server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + container_dir + )) + } + })() + .await + .log_err() +} + pub struct EsLintLspAdapter { node: Arc, } @@ -204,12 +222,13 @@ impl LspAdapter for EsLintLspAdapter { async fn fetch_latest_server_version( &self, - http: Arc, + delegate: &dyn LspAdapterDelegate, ) -> Result> { // At the time of writing the latest vscode-eslint release was released in 2020 and requires // special custom LSP protocol extensions be handled to fully initialize. Download the latest // prerelease instead to sidestep this issue - let release = latest_github_release("microsoft/vscode-eslint", true, http).await?; + let release = + latest_github_release("microsoft/vscode-eslint", true, delegate.http_client()).await?; Ok(Box::new(GitHubLspBinaryVersion { name: release.name, url: release.tarball_url, @@ -219,8 +238,8 @@ impl LspAdapter for EsLintLspAdapter { async fn fetch_server_binary( &self, version: Box, - http: Arc, container_dir: PathBuf, + delegate: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); let destination_path = container_dir.join(format!("vscode-eslint-{}", version.name)); @@ -229,7 +248,8 @@ impl LspAdapter for EsLintLspAdapter { if fs::metadata(&server_path).await.is_err() { remove_matching(&container_dir, |entry| entry != destination_path).await; - let mut response = http + let mut response = delegate + .http_client() .get(&version.url, Default::default(), true) .await .map_err(|err| anyhow!("error downloading release: {}", err))?; @@ -243,11 +263,11 @@ impl LspAdapter for EsLintLspAdapter { fs::rename(first.path(), &repo_root).await?; self.node - .run_npm_subcommand(&repo_root, "install", &[]) + .run_npm_subcommand(Some(&repo_root), "install", &[]) .await?; self.node - .run_npm_subcommand(&repo_root, "run-script", &["compile"]) + .run_npm_subcommand(Some(&repo_root), "run-script", &["compile"]) .await?; } @@ -257,22 +277,19 @@ impl LspAdapter for EsLintLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - // This is unfortunate but we don't know what the version is to build a path directly - let mut dir = fs::read_dir(&container_dir).await?; - let first = dir.next().await.ok_or(anyhow!("missing first file"))??; - if !first.file_type().await?.is_dir() { - return Err(anyhow!("First entry is not a directory")); - } + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_eslint_server_binary(container_dir, &self.node).await + } - Ok(LanguageServerBinary { - path: first.path().join(Self::SERVER_PATH), - arguments: Default::default(), - }) - })() - .await - .log_err() + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_eslint_server_binary(container_dir, &self.node).await } async fn label_for_completion( @@ -288,6 +305,28 @@ impl LspAdapter for EsLintLspAdapter { } } +async fn get_cached_eslint_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + // This is unfortunate but we don't know what the version is to build a path directly + let mut dir = fs::read_dir(&container_dir).await?; + let first = dir.next().await.ok_or(anyhow!("missing first file"))??; + if !first.file_type().await?.is_dir() { + return Err(anyhow!("First entry is not a directory")); + } + let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH); + + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: eslint_server_binary_arguments(&server_path), + }) + })() + .await + .log_err() +} + #[cfg(test)] mod tests { use gpui::TestAppContext; diff --git a/crates/zed/src/languages/yaml.rs b/crates/zed/src/languages/yaml.rs index 7f87a7caed..b57c6f5699 100644 --- a/crates/zed/src/languages/yaml.rs +++ b/crates/zed/src/languages/yaml.rs @@ -3,8 +3,9 @@ use async_trait::async_trait; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{ - language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter, + language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate, }; +use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use serde_json::Value; use smol::fs; @@ -15,9 +16,10 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use util::http::HttpClient; use util::ResultExt; +const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server"; + fn server_binary_arguments(server_path: &Path) -> Vec { vec![server_path.into(), "--stdio".into()] } @@ -27,8 +29,6 @@ pub struct YamlLspAdapter { } impl YamlLspAdapter { - const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server"; - pub fn new(node: Arc) -> Self { YamlLspAdapter { node } } @@ -42,7 +42,7 @@ impl LspAdapter for YamlLspAdapter { async fn fetch_latest_server_version( &self, - _: Arc, + _: &dyn LspAdapterDelegate, ) -> Result> { Ok(Box::new( self.node @@ -54,11 +54,11 @@ impl LspAdapter for YamlLspAdapter { async fn fetch_server_binary( &self, version: Box, - _: Arc, container_dir: PathBuf, + _: &dyn LspAdapterDelegate, ) -> Result { let version = version.downcast::().unwrap(); - let server_path = container_dir.join(Self::SERVER_PATH); + let server_path = container_dir.join(SERVER_PATH); if fs::metadata(&server_path).await.is_err() { self.node @@ -72,34 +72,20 @@ impl LspAdapter for YamlLspAdapter { }) } - async fn cached_server_binary(&self, container_dir: PathBuf) -> Option { - (|| async move { - let mut last_version_dir = None; - let mut entries = fs::read_dir(&container_dir).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - if entry.file_type().await?.is_dir() { - last_version_dir = Some(entry.path()); - } - } - let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let server_path = last_version_dir.join(Self::SERVER_PATH); - if server_path.exists() { - Ok(LanguageServerBinary { - path: self.node.binary_path().await?, - arguments: server_binary_arguments(&server_path), - }) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - })() - .await - .log_err() + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await } + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + get_cached_server_binary(container_dir, &self.node).await + } fn workspace_configuration(&self, cx: &mut AppContext) -> Option> { let tab_size = all_language_settings(None, cx) .language(Some("YAML")) @@ -117,3 +103,34 @@ impl LspAdapter for YamlLspAdapter { ) } } + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 73a3346a9a..3da8c24617 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -31,7 +31,6 @@ use std::{ ffi::OsStr, fs::OpenOptions, io::Write as _, - ops::Not, os::unix::prelude::OsStrExt, panic, path::{Path, PathBuf}, @@ -49,6 +48,7 @@ use util::{ http::{self, HttpClient}, paths::PathLikeWithPosition, }; +use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; @@ -69,9 +69,8 @@ fn main() { log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); - init_panic_hook(&app); - - app.background(); + let installation_id = app.background().block(installation_id()).ok(); + init_panic_hook(&app, installation_id.clone()); load_embedded_fonts(&app); @@ -132,7 +131,7 @@ fn main() { languages.set_executor(cx.background().clone()); languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone()); let languages = Arc::new(languages); - let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned()); + let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned()); languages::init(languages.clone(), node_runtime.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); @@ -155,7 +154,6 @@ fn main() { search::init(cx); vim::init(cx); terminal_view::init(cx); - theme_testbench::init(cx); copilot::init(http.clone(), node_runtime, cx); ai::init(cx); @@ -170,7 +168,7 @@ fn main() { }) .detach(); - client.telemetry().start(); + client.telemetry().start(installation_id); let app_state = Arc::new(AppState { languages, @@ -182,6 +180,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); @@ -270,6 +270,22 @@ fn main() { }); } +async fn installation_id() -> Result { + let legacy_key_name = "device_id"; + + if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) { + Ok(installation_id) + } else { + let installation_id = Uuid::new_v4().to_string(); + + KEY_VALUE_STORE + .write_kvp(legacy_key_name.to_string(), installation_id.clone()) + .await?; + + Ok(installation_id) + } +} + fn open_urls( urls: Vec, cli_connections_tx: &mpsc::UnboundedSender<( @@ -373,7 +389,8 @@ struct Panic { os_version: Option, architecture: String, panicked_on: u128, - identifying_backtrace: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + installation_id: Option, } #[derive(Serialize)] @@ -382,7 +399,7 @@ struct PanicRequest { token: String, } -fn init_panic_hook(app: &App) { +fn init_panic_hook(app: &App, installation_id: Option) { let is_pty = stdout_is_a_pty(); let platform = app.platform(); @@ -401,61 +418,18 @@ fn init_panic_hook(app: &App) { .unwrap_or_else(|| "Box".to_string()); let backtrace = Backtrace::new(); - let backtrace = backtrace + let mut backtrace = backtrace .frames() .iter() - .filter_map(|frame| { - let symbol = frame.symbols().first()?; - let path = symbol.filename()?; - Some((path, symbol.lineno(), format!("{:#}", symbol.name()?))) - }) + .filter_map(|frame| Some(format!("{:#}", frame.symbols().first()?.name()?))) .collect::>(); - let this_file_path = Path::new(file!()); - - // Find the first frame in the backtrace for this panic hook itself. Exclude - // that frame and all frames before it. - let mut start_frame_ix = 0; - let mut codebase_root_path = None; - for (ix, (path, _, _)) in backtrace.iter().enumerate() { - if path.ends_with(this_file_path) { - start_frame_ix = ix + 1; - codebase_root_path = path.ancestors().nth(this_file_path.components().count()); - break; - } - } - - // Exclude any subsequent frames inside of rust's panic handling system. - while let Some((path, _, _)) = backtrace.get(start_frame_ix) { - if path.starts_with("/rustc") { - start_frame_ix += 1; - } else { - break; - } - } - - // Build two backtraces: - // * one for display, which includes symbol names for all frames, and files - // and line numbers for symbols in this codebase - // * one for identification and de-duplication, which only includes symbol - // names for symbols in this codebase. - let mut display_backtrace = Vec::new(); - let mut identifying_backtrace = Vec::new(); - for (path, line, symbol) in &backtrace[start_frame_ix..] { - display_backtrace.push(symbol.clone()); - - if let Some(codebase_root_path) = &codebase_root_path { - if let Ok(suffix) = path.strip_prefix(&codebase_root_path) { - identifying_backtrace.push(symbol.clone()); - - let display_path = suffix.to_string_lossy(); - if let Some(line) = line { - display_backtrace.push(format!(" {display_path}:{line}")); - } else { - display_backtrace.push(format!(" {display_path}")); - } - } - } + // Strip out leading stack frames for rust panic-handling. + if let Some(ix) = backtrace + .iter() + .position(|name| name == "rust_begin_unwind") + { + backtrace.drain(0..=ix); } let panic_data = Panic { @@ -477,29 +451,28 @@ fn init_panic_hook(app: &App) { .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), - backtrace: display_backtrace, - identifying_backtrace: identifying_backtrace - .is_empty() - .not() - .then_some(identifying_backtrace), + backtrace, + installation_id: installation_id.clone(), }; - if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { - if is_pty { + if is_pty { + if let Some(panic_data_json) = serde_json::to_string_pretty(&panic_data).log_err() { eprintln!("{}", panic_data_json); return; } - - let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); - let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp)); - let panic_file = std::fs::OpenOptions::new() - .append(true) - .create(true) - .open(&panic_file_path) - .log_err(); - if let Some(mut panic_file) = panic_file { - write!(&mut panic_file, "{}", panic_data_json).log_err(); - panic_file.flush().log_err(); + } else { + if let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { + let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); + let panic_file_path = paths::LOGS_DIR.join(format!("zed-{}.panic", timestamp)); + let panic_file = std::fs::OpenOptions::new() + .append(true) + .create(true) + .open(&panic_file_path) + .log_err(); + if let Some(mut panic_file) = panic_file { + writeln!(&mut panic_file, "{}", panic_data_json).log_err(); + panic_file.flush().log_err(); + } } } })); @@ -531,23 +504,45 @@ fn upload_previous_panics(http: Arc, cx: &mut AppContext) { } if telemetry_settings.diagnostics { - let panic_data_text = smol::fs::read_to_string(&child_path) + let panic_file_content = smol::fs::read_to_string(&child_path) .await .context("error reading panic file")?; - let body = serde_json::to_string(&PanicRequest { - panic: serde_json::from_str(&panic_data_text)?, - token: ZED_SECRET_CLIENT_TOKEN.into(), - }) - .unwrap(); + let panic = serde_json::from_str(&panic_file_content) + .ok() + .or_else(|| { + panic_file_content + .lines() + .next() + .and_then(|line| serde_json::from_str(line).ok()) + }) + .unwrap_or_else(|| { + log::error!( + "failed to deserialize panic file {:?}", + panic_file_content + ); + None + }); - let request = Request::post(&panic_report_url) - .redirect_policy(isahc::config::RedirectPolicy::Follow) - .header("Content-Type", "application/json") - .body(body.into())?; - let response = http.send(request).await.context("error sending panic")?; - if !response.status().is_success() { - log::error!("Error uploading panic to server: {}", response.status()); + if let Some(panic) = panic { + let body = serde_json::to_string(&PanicRequest { + panic, + token: ZED_SECRET_CLIENT_TOKEN.into(), + }) + .unwrap(); + + let request = Request::post(&panic_report_url) + .redirect_policy(isahc::config::RedirectPolicy::Follow) + .header("Content-Type", "application/json") + .body(body.into())?; + let response = + http.send(request).await.context("error sending panic")?; + if !response.status().is_success() { + log::error!( + "Error uploading panic to server: {}", + response.status() + ); + } } } @@ -896,6 +891,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { ("Go to file", &file_finder::Toggle), ("Open command palette", &command_palette::Toggle), ("Open recent projects", &recent_projects::OpenRecent), - ("Change your settings", &zed::OpenSettings), + ("Change your settings", &zed_actions::OpenSettings), ] } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 088d26be78..0df16f4bab 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -20,7 +20,6 @@ use feedback::{ }; use futures::{channel::mpsc, StreamExt}; use gpui::{ - actions, anyhow::{self, Result}, geometry::vector::vec2f, impl_actions, @@ -50,6 +49,7 @@ use workspace::{ notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, }; +use zed_actions::*; #[derive(Deserialize, Clone, PartialEq)] pub struct OpenBrowser { @@ -58,33 +58,6 @@ pub struct OpenBrowser { impl_actions!(zed, [OpenBrowser]); -actions!( - zed, - [ - About, - Hide, - HideOthers, - ShowAll, - Minimize, - Zoom, - ToggleFullScreen, - Quit, - DebugElements, - OpenLog, - OpenLicenses, - OpenTelemetryLog, - OpenKeymap, - OpenSettings, - OpenLocalSettings, - OpenDefaultSettings, - OpenDefaultKeymap, - IncreaseBufferFontSize, - DecreaseBufferFontSize, - ResetBufferFontSize, - ResetDatabase, - ] -); - pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { cx.add_action(about); cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| { @@ -361,15 +334,15 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - let assistant_panel = if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable { - None - } else { - Some(AssistantPanel::load(workspace_handle.clone(), cx.clone()).await?) - }; - let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?; + let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + let (project_panel, terminal_panel, assistant_panel) = + futures::try_join!(project_panel, terminal_panel, assistant_panel)?; workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); workspace.add_panel(project_panel, cx); + workspace.add_panel(terminal_panel, cx); + workspace.add_panel(assistant_panel, cx); + if !was_deserialized && workspace .project() @@ -383,11 +356,7 @@ pub fn initialize_workspace( { workspace.toggle_dock(project_panel_position, cx); } - - workspace.add_panel(terminal_panel, cx); - if let Some(assistant_panel) = assistant_panel { - workspace.add_panel(assistant_panel, cx); - } + cx.focus_self(); })?; Ok(()) }) @@ -739,8 +708,8 @@ mod tests { use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use fs::{FakeFs, Fs}; use gpui::{ - elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, AssetSource, - Element, Entity, TestAppContext, View, ViewHandle, + actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, + AssetSource, Element, Entity, TestAppContext, View, ViewHandle, }; use language::LanguageRegistry; use node_runtime::NodeRuntime; @@ -2105,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) -> AnyElement { + 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, @@ -2175,7 +2305,7 @@ mod tests { languages.set_executor(cx.background().clone()); let languages = Arc::new(languages); let http = FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::new(http, cx.background().to_owned()); + let node_runtime = NodeRuntime::instance(http, cx.background().to_owned()); languages::init(languages.clone(), node_runtime); for name in languages.language_names() { languages.language_for_name(&name); @@ -2191,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); diff --git a/docs/backend-development.md b/docs/backend-development.md new file mode 100644 index 0000000000..59100d3628 --- /dev/null +++ b/docs/backend-development.md @@ -0,0 +1,52 @@ +[⬅ Back to Index](./index.md) + +# Developing Zed's Backend + +Zed's backend consists of the following components: + +- The Zed.dev web site + - implemented in the [`zed.dev`](https://github.com/zed-industries/zed.dev) repository + - hosted on [Vercel](https://vercel.com/zed-industries/zed-dev). +- The Zed Collaboration server + - implemented in the [`crates/collab`](https://github.com/zed-industries/zed/tree/main/crates/collab) directory of the main `zed` repository + - hosted on [DigitalOcean](https://cloud.digitalocean.com/projects/6c680a82-9d3b-4f1a-91e5-63a6ca4a8611), using Kubernetes +- The Zed Postgres database + - defined via migrations in the [`crates/collab/migrations`](https://github.com/zed-industries/zed/tree/main/crates/collab/migrations) directory + - hosted on DigitalOcean + +--- + +## Local Development + +Here's some things you need to develop backend code locally. + +### Dependencies + +- **Postgres** - download [Postgres.app](https://postgresapp.com). + +### Setup + +1. Check out the `zed` and `zed.dev` repositories into a common parent directory +2. Set the `GITHUB_TOKEN` environment variable to one of your GitHub personal access tokens (PATs). + + - You can create a PAT [here](https://github.com/settings/tokens). + - You may want to add something like this to your `~/.zshrc`: + + ``` + export GITHUB_TOKEN= + ``` + +3. In the `zed.dev` directory, run `npm install` to install dependencies. +4. In the `zed directory`, run `script/bootstrap` to set up the database +5. In the `zed directory`, run `foreman start` to start both servers + +--- + +## Production Debugging + +### Datadog + +Zed uses Datadog to collect metrics and logs from backend services. The Zed organization lives within Datadog's _US5_ [site](https://docs.datadoghq.com/getting_started/site/), so it can be accessed at [us5.datadoghq.com](https://us5.datadoghq.com). Useful things to look at in Datadog: + +- The [Logs](https://us5.datadoghq.com/logs) page shows logs from Zed.dev and the Collab server, and the internals of Zed's Kubernetes cluster. +- The [collab metrics dashboard](https://us5.datadoghq.com/dashboard/y2d-gxz-h4h/collab?from_ts=1660517946462&to_ts=1660604346462&live=true) shows metrics about the running collab server diff --git a/docs/building-zed.md b/docs/building-zed.md new file mode 100644 index 0000000000..78653571ad --- /dev/null +++ b/docs/building-zed.md @@ -0,0 +1,79 @@ +[⬅ Back to Index](./index.md) + +# Building Zed + +How to build Zed from source for the first time. + +## Process + +Expect this to take 30min to an hour! Some of these steps will take quite a while based on your connection speed, and how long your first build will be. + +1. Install the [GitHub CLI](https://cli.github.com/): + - `brew install gh` +1. Clone the `zed` repo + - `gh repo clone zed-industries/zed` +1. Install Xcode from the macOS App Store +1. Install [Postgres](https://postgresapp.com) +1. Install rust/rustup + - `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` +1. Install the wasm toolchain + - `rustup target add wasm32-wasi` +1. Generate an GitHub API Key + - Go to https://github.com/settings/tokens and Generate new token + - GitHub currently provides two kinds of tokens: + - Classic Tokens, where only `repo` (Full control of private repositories) OAuth scope has to be selected + Unfortunately, unselecting `repo` scope and selecting every its inner scope instead does not allow the token users to read from private repositories + - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos + - Keep the token in the browser tab/editor for the next two steps +1. Open Postgres.app +1. From `./path/to/zed/`: + - Run: + - `GITHUB_TOKEN={yourGithubAPIToken} script/bootstrap` + - Replace `{yourGithubAPIToken}` with the API token you generated above. + - Consider removing the token (if it's fine for you to crecreate such tokens during occasional migrations) or store this token somewhere safe (like your Zed 1Password vault). + - If you get: + - ```bash + Error: Cannot install in Homebrew on ARM processor in Intel default prefix (/usr/local)! + Please create a new installation in /opt/homebrew using one of the + "Alternative Installs" from: + https://docs.brew.sh/Installation + ``` + - In that case try: + - `/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"` + - If Homebrew is not in your PATH: + - Replace `{username}` with your home folder name (usually your login name) + - `echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> /Users/{username}/.zprofile` + - `eval "$(/opt/homebrew/bin/brew shellenv)"` +1. To run the Zed app: + - If you are working on zed: + - `cargo run` + - If you are just using the latest version, but not working on zed: + - `cargo run --release` + - If you need to run the collaboration server locally: + - `script/zed-with-local-servers` + +## Troubleshooting + +### `error: failed to run custom build command for gpui v0.1.0 (/Users/path/to/zed)` + +- Try `xcode-select --switch /Applications/Xcode.app/Contents/Developer` + +### `xcrun: error: unable to find utility "metal", not a developer tool or in PATH` + +### Seeding errors during `script/bootstrap` runs + +``` +seeding database... +thread 'main' panicked at 'failed to deserialize github user from 'https://api.github.com/orgs/zed-industries/teams/staff/members': reqwest::Error { kind: Decode, source: Error("invalid type: map, expected a sequence", line: 1, column: 0) }', crates/collab/src/bin/seed.rs:111:10 +``` + +Wrong permissions for `GITHUB_TOKEN` token used, the token needs to be able to read from private repos. +For Classic GitHub Tokens, that required OAuth scope `repo` (seacrh the scope name above for more details) + +Same command + +`sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer` + +### If you experience errors that mention some dependency is using unstable features + +Try `cargo clean` and `cargo build` diff --git a/docs/company-and-vision.md b/docs/company-and-vision.md new file mode 100644 index 0000000000..389d0774a7 --- /dev/null +++ b/docs/company-and-vision.md @@ -0,0 +1,34 @@ +[⬅ Back to Index](./index.md) + +# Company & Vision + +## Vision + +Our goal is to make Zed the primary tool software teams use to collaborate. + +To do this, Zed will... + +* Make collaboration a first-class feature of the code authoring environment. +* Enable text-based conversations about any piece of text, independent of whether/when it was committed to version control. +* Make it smooth to edit and discuss code with teammates in real time. +* Make it easy to recall past conversations any area of the code. + +We believe the best way to make collaboration amazing is to build it into a new editor rather than retrofitting an existing editor. This means that in order for a team to adopt Zed for collaboration, each team member will need to adopt it as their editor as well. + +For this reason, we need to deliver a clearly superior experience as a single-user code editor in addition to being an excellent collaboration tool. This will take time, but we believe the dominance of VS Code demonstrates that it's possible for a single tool to capture substantial market share. We can proceed incrementally, capturing one team at a time and gradually transitioning conversations away from GitHub. + +## Zed Values + +Everyone wants to work quickly and have a lot of users. What are we unwilling to sacrifice in pursuit of those goals? + +- **Performance.** Speed is core to our brand and value proposition. It's important that we consistently deliver a response in less than 8ms on modern hardware for fine-grained actions. Coarse-grained actions should render feedback within 50ms. We consider the performance goals of the product at all times, and take the time to ensure our code meets them with reasonable usage. Once we have met our goals, we assess the impact vs effort of further performance investment and know when to say when. We measure our performance in the field and make an effort to maintain or improve real-world performance and promptly address regressions. + +- **Craftsmanship.** Zed is a premium product, and we put care into design and user experience. We can always cut scope, but what we do ship should be quality. Incomplete is okay, so long as we're executing on a coherent subset well. Half-baked, unintuitive, or broken is not okay. + +- **Shipping.** Knowledge matters only in as much as it drives results. We're here to build a real product in the real world. We care a lot about the experience of developing Zed, but we care about the user's experience more. + +- **Code quality.** This enables craftsmanship. Nobody is creative in a trash heap, and we're willing to dedicate time to keep our codebase clean. If we're spending no time refactoring, we are likely underinvesting. When we realize a design flaw, we assess its centrality to the rest of the system and consider budgeting time to address it. If we're spending all of our time refactoring, we are likely either overinvesting or paying off debt from past underinvestment. It's up to each engineer to allocate a reasonable refactoring budget. We shouldn't be navel gazing, but we also shouldn't be afraid to invest. + +- **Pairing.** Zed depends on regular pair programming to promote cohesion on our remote team. We believe pairing is a powerful substitute for beuracratic management, excessive documentation, and tedious code review. Nobody has to pair all day, every day, but everyone is responsible for pairing at least 2 hours a week with a variety of other engineers. If anyone wants to pair all day every day, that is explicitly endorsed and credited. If pairing temporarily reduces our throughput due to working on one thing instead of two, we trust that it will pay for itself in the long term by increasing our velocity and allowing us to more effectively grow our team. + +- **Long-term thinking.** The Zed vision began several years ago, and we expect Zed to be around many years from today. We must always be mindful to avoid overengineering for the future, but we should also keep the long-term in mind. Are we building a system our future selves would want to work on in 5 years? diff --git a/docs/design-tools.md b/docs/design-tools.md new file mode 100644 index 0000000000..70e5459811 --- /dev/null +++ b/docs/design-tools.md @@ -0,0 +1,74 @@ +[⬅ Back to Index](./index.md) + +# Design Tools & Links + +Generally useful tools and resources for design. + +## General + +[Names of Signs & Symbols](https://www.prepressure.com/fonts/basics/character-names#curlybrackets) + +[The Noun Project](https://thenounproject.com/) - Icons for everything, attempts to describe all of human language visually. + +[SVG Repo](https://www.svgrepo.com/) - Open-licensed SVG Vector and Icons + +[Font Awsesome](https://fontawesome.com/) - High quality icons, has been around for many years. + +--- + +## Color + +[Opacity/Transparency Hex Values](https://gist.github.com/lopspower/03fb1cc0ac9f32ef38f4) + +[Color Ramp Generator](https://lyft-colorbox.herokuapp.com) + +[Designing a Comprehensive Color System +](https://www.rethinkhq.com/videos/designing-a-comprehensive-color-system-for-lyft) - [Linda Dong](https://twitter.com/lindadong) + +--- + +## Figma & Plugins + +[Figma Plugins for Designers](https://www.uiprep.com/blog/21-best-figma-plugins-for-designers-in-2021) + +[Icon Resizer](https://www.figma.com/community/plugin/739117729229117975/Icon-Resizer) + +[Code Syntax Highlighter](https://www.figma.com/community/plugin/938793197191698232/Code-Syntax-Highlighter) + +[Proportional Scale](https://www.figma.com/community/plugin/756895186298946525/Proportional-Scale) + +[LilGrid](https://www.figma.com/community/plugin/795397421598343178/LilGrid) + +Organize your selection into a grid. + +[Automator](https://www.figma.com/community/plugin/1005114571859948695/Automator) + +Build photoshop-style batch actions to automate things. + +[Figma Tokens](https://www.figma.com/community/plugin/843461159747178978/Figma-Tokens) + +Use tokens in Figma and generate JSON from them. + +--- + +## Design Systems + +### Naming + +[Naming Design Tokens](https://uxdesign.cc/naming-design-tokens-9454818ed7cb) + +### Storybook + +[Collaboration with design tokens and storybook](https://zure.com/blog/collaboration-with-design-tokens-and-storybook/) + +### Example DS Documentation + +[Tailwind CSS Documentation](https://tailwindcss.com/docs/container) + +[Material Design Docs](https://material.io/design/color/the-color-system.html#color-usage-and-palettes) + +[Carbon Design System Docs](https://www.carbondesignsystem.com) + +[Adobe Spectrum](https://spectrum.adobe.com/) + - Great documentation, like [Color System](https://spectrum.adobe.com/page/color-system/) and [Design Tokens](https://spectrum.adobe.com/page/design-tokens/). + - A good place to start if thinking about building a design system. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000..0fc5bfbc89 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,14 @@ +[⬅ Back to Index](./index.md) + +# Welcome to Zed + +Welcome! These internal docs are a work in progress. You can contribute to them by submitting a PR directly! + +## Contents + +- [The Company](./company-and-vision.md) +- [Tools We Use](./tools.md) +- [Building Zed](./building-zed.md) +- [Release Process](./release-process.md) +- [Backend Development](./backend-development.md) +- [Design Tools & Links](./design-tools.md) diff --git a/docs/local-collaboration.md b/docs/local-collaboration.md new file mode 100644 index 0000000000..7d8054af67 --- /dev/null +++ b/docs/local-collaboration.md @@ -0,0 +1,22 @@ +# Local Collaboration + +## Setting up the local collaboration server + +### Setting up for the first time? + +1. Make sure you have livekit installed (`brew install livekit`) +1. Install [Postgres](https://postgresapp.com) and run it. +1. Then, from the root of the repo, run `script/bootstrap`. + +### Have a db that is out of date? / Need to migrate? + +1. Make sure you have livekit installed (`brew install livekit`) +1. Try `cd crates/collab && cargo run -- migrate` from the root of the repo. +1. Run `script/seed-db` + +## Testing collab locally + +1. Run `foreman start` from the root of the repo. +1. In another terminal run `script/start-local-collaboration`. +1. Two copies of Zed will open. Add yourself as a contact in the one that is not you. +1. Start a collaboration session as normal with any open project. diff --git a/docs/release-process.md b/docs/release-process.md new file mode 100644 index 0000000000..ce43d647bd --- /dev/null +++ b/docs/release-process.md @@ -0,0 +1,96 @@ +[⬅ Back to Index](./index.md) + +# Zed's Release Process + +The process to create and ship a Zed release + +## Overview + +### Release Channels + +Users of Zed can choose between two _release channels_ - 'Stable' and 'Preview'. Most people use Stable, but Preview exists so that the Zed team and other early-adopters can test new features before they are released to our general user-base. + +### Weekly (Minor) Releases + +We normally publish new releases of Zed on Wednesdays, for both the Stable and Preview channels. For each of these releases, we bump Zed's _minor_ version number. + +For the Preview channel, we build the new release based on what's on the `main` branch. For the Stable channel, we build the new release based on the last Preview release. + +### Hotfix (Patch) Releases + +When we find a _regression_ in Zed (a bug that wasn't present in an earlier version), or find a significant bug in a newly-released feature, we typically publish a hotfix release. For these releases, we bump Zed's _patch_ version number. + +### Server Deployments + +Often, changes in the Zed app require corresponding changes in the `collab` server. At the currente stage of our copmany, we don't attempt to keep our server backwards-compatible with older versions of the app. Instead, when making a change, we simply bump Zed's _protocol version_ number (in the `rpc` crate), which causes the server to recognize that it isn't compatible with earlier versions of the Zed app. + +This means that when releasing a new version of Zed that has changes to the RPC protocol, we need to deploy a new version of the `collab` server at the same time. + +## Instructions + +### Publishing a Minor Release + +1. Announce your intent to publish a new version in Discord. This gives other people a chance to raise concerns or postpone the release if they want to get something merged before publishing a new version. +1. Open your terminal and `cd` into your local copy of Zed. Checkout `main` and perform a `git pull` to ensure you have the latest version. +1. Run the following command, which will update two git branches and two git tags (one for each release channel): + + ``` + script/bump-zed-minor-versions + ``` + +1. The script will make local changes only, and print out a shell command that you can use to push all of these branches and tags. +1. Pushing the two new tags will trigger two CI builds that, when finished, will create two draft releases (Stable and Preview) containing `Zed.dmg` files. +1. Now you need to write the release notes for the Stable and Preview releases. For the Stable release, you can just copy the release notes from the last week's Preview release, plus any hotfixes that were published on the Preview channel since then. Some of the hotfixes may not be relevant for the Stable release notes, if they were fixing bugs that were only present in Preview. +1. For the Preview release, you can retrieve the list of changes by running this command (make sure you have at least `Node 18` installed): + + ``` + GITHUB_ACCESS_TOKEN=your_access_token script/get-preview-channel-changes + ``` + +1. The script will list all the merged pull requests and you can use it as a reference to write the release notes. If there were protocol changes, it will also emit a warning. +1. Once CI creates the draft releases, add each release's notes and save the drafts. +1. If there have been server-side changes since the last release, you'll need to re-deploy the `collab` server. See below. +1. Before publishing, download the Zed.dmg and smoke test it to ensure everything looks good. + +### Publishing a Patch Release + +1. Announce your intent to publish a new patch version in Discord. +1. Open your terminal and `cd` into your local copy of Zed. Check out the branch corresponding to the release channel where the fix is needed. For example, if the fix is for a bug in Stable, and the current stable version is `0.63.0`, then checkout the branch `v0.63.x`. Run `git pull` to ensure your branch is up-to-date. +1. Find the merge commit where your bug-fix landed on `main`. You can browse the merged pull requests on main by running `git log main --grep Merge`. +1. Cherry-pick those commits onto the current release branch: + + ``` + git cherry-pick -m1 + ``` + +1. Run the following command, which will bump the version of Zed and create a new tag: + + ``` + script/bump-zed-patch-version + ``` + +1. The script will make local changes only, and print out a shell command that you can use to push all the branch and tag. +1. Pushing the new tag will trigger a CI build that, when finished, will create a draft release containing a `Zed.dmg` file. +1. Once the draft release is created, fill in the release notes based on the bugfixes that you cherry-picked. +1. If any of the bug-fixes require server-side changes, you'll need to re-deploy the `collab` server. See below. +1. Before publishing, download the Zed.dmg and smoke test it to ensure everything looks good. +1. Clicking publish on the release will cause old Zed instances to auto-update and the Zed.dev releases page to re-build and display the new release. + +### Deploying the Server + +1. Deploying the server is a two-step process that begins with pushing a tag. 1. Check out the commit you'd like to deploy. Often it will be the head of `main`, but could be on any branch. +1. Run the following script, which will bump the version of the `collab` crate and create a new tag. The script takes an argument `minor` or `patch`, to indicate how to increment the version. If you're releasing new features, use `minor`. If it's just a bugfix, use `patch` + + ``` + script/bump-collab-version patch + ``` + +1. This script will make local changes only, and print out a shell command that you can use to push the branch and tag. +1. Pushing the new tag will trigger a CI build that, when finished will upload a new versioned docker image to the DigitalOcean docker registry. +1. Once that CI job completes, you will be able to run the following command to deploy that docker image. The script takes two arguments: an environment (`production`, `preview`, or `staging`), and a version number (e.g. `0.10.1`). + + ``` + script/deploy preview 0.10.1 + ``` + +1. This command should complete quickly, updating the given environment to use the given version number of the `collab` server. diff --git a/docs/tools.md b/docs/tools.md new file mode 100644 index 0000000000..6e424a6f81 --- /dev/null +++ b/docs/tools.md @@ -0,0 +1,82 @@ +[⬅ Back to Index](./index.md) + +# Tools + +Tools to get started at Zed. Work in progress, submit a PR to add any missing tools here! + +--- + +## Everyday Tools + +### Calendar + +To gain access to company calendar, visit [this link](https://calendar.google.com/calendar/u/0/r?cid=Y18xOGdzcGE1aG5wdHJocGRoNWtlb2tlbWxzc0Bncm91cC5jYWxlbmRhci5nb29nbGUuY29t). + +If you would like the company calendar to be synced with a calendar application (Apple Calendar, etc.): + +- Add your company account (i.e `joseph@zed.dev`) to your calendar application +- Visit [this link](https://calendar.google.com/calendar/u/0/syncselect), check `Zed Industries (Read Only)` under `Shared Calendars`, and save it. + +### 1Password + +We have a shared company 1Password with all of our credentials. To gain access: + +1. Go to [zed-industries.1password.com](https://zed-industries.1password.com). +1. Sign in with your `@zed.dev` email address. +1. Make your account and let an admin know you've signed up. +1. Once they approve your sign up, you'll have access to all of the company credentials. + +### Slack + +Have a team member add you to the [Zed Industries](https://zed-industries.slack.com/) slack. + +### Discord + +We have a discord community. You can use [this link](https://discord.gg/SSD9eJrn6s) to join. **!Don't share this link, this is specifically for team memebers!** + +Once you have joined the community, let a team member know and we can add your correct role. + +--- + +## Engineering + +### Github + +For now, all the Zed source code lives on [Github](https://github.com/zed-industries). A founder will need to add you to the team and set up the appropriate permissions. + +Useful repos: +- [zed-industries/zed](https://github.com/zed-industries/zed) - Zed source +- [zed-industries/zed.dev](https://github.com/zed-industries/zed.dev) - Zed.dev site and collab API +- [zed-industries/docs](https://github.com/zed-industries/docs) - Zed public docs +- [zed-industries/community](https://github.com/zed-industries/community) - Zed community feedback & discussion + +### Vercel + +We use Vercel for all of our web deployments and some backend things. If you sign up with your `@zed.dev` email you should be invited to join the team automatically. If not, ask a founder to invite you to the Vercel team. + +### Environment Variables + +You can get access to many of our shared enviroment variables through 1Password and Vercel. For one password search the value you are looking for, or sort by passwords or API credentials. + +For Vercel, go to `settings` -> `Environment Variables` (either on the entire org, or on a specific project depending on where it is shared.) For a given Vercel project if you have their CLI installed you can use `vercel pull` or `vercel env` to pull values down directly. More on those in their [CLI docs](https://vercel.com/docs/cli/env). + +--- + +## Design + +### Figma + +We use Figma for all of our design work. To gain access: + +1. Use [this link](https://www.figma.com/team_invite/redeem/Xg4RcNXHhwP5netIvVBmKQ) to join the Figma team. +1. You should now have access to all of the company files. +1. You should go to the team page and "favorite" (star) any relevant projects so they show up in your sidebar. +1. Download the [Figma app](https://www.figma.com/downloads/) for easier access on desktop. + +### Campsite + +We use Campsite to review and discuss designs. To gain access: + +1. Download the [Campsite app](https://campsite.design/desktop/download). +1. Open it and sign in with your `@zed.dev` email address. +1. You can access our company campsite directly: [app.campsite.design/zed](https://app.campsite.design/zed) diff --git a/docs/zed/syntax-highlighting.md b/docs/zed/syntax-highlighting.md new file mode 100644 index 0000000000..d4331ee367 --- /dev/null +++ b/docs/zed/syntax-highlighting.md @@ -0,0 +1,79 @@ +# Syntax Highlighting in Zed + +This doc is a work in progress! + +## Defining syntax highlighting rules + +We use tree-sitter queries to match certian properties to highlight. + +### Simple Example: + +```scheme +(property_identifier) @property +``` + +```ts +const font: FontFamily = { + weight: "normal", + underline: false, + italic: false, +} +``` + +Match a property identifier and highlight it using the identifier `@property`. In the above example, `weight`, `underline`, and `italic` would be highlighted. + +### Complex example: + +```scheme +(_ + return_type: (type_annotation + [ + (type_identifier) @type.return + (generic_type + name: (type_identifier) @type.return) + ])) +``` + +```ts +function buildDefaultSyntax(colorScheme: Theme): Partial { + // ... +} +``` + +Match a function return type, and highlight the type using the identifier `@type.return`. In the above example, `Partial` would be highlighted. + +### Example - Typescript + +Here is an example portion of our `highlights.scm` for TypeScript: + +```scheme +; crates/zed/src/languages/typescript/highlights.scm + +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +; ... +``` diff --git a/script/build-theme-types b/script/build-theme-types new file mode 100755 index 0000000000..b78631f3d1 --- /dev/null +++ b/script/build-theme-types @@ -0,0 +1,10 @@ +#!/bin/bash + +echo "running xtask" +(cd crates/theme && cargo xtask build-theme-types) + +echo "updating theme packages" +(cd styles && npm install) + +echo "building theme types" +(cd styles && npm run build-types) diff --git a/script/start-local-collaboration b/script/start-local-collaboration index b8632c4c22..b702fb4e02 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -54,5 +54,5 @@ sleep 0.5 # Start the two Zed child processes. Open the given paths with the first instance. trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT ZED_IMPERSONATE=${username_1} ZED_WINDOW_POSITION=${position_1} target/debug/Zed $@ & -ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & +SECOND=true ZED_IMPERSONATE=${username_2} ZED_WINDOW_POSITION=${position_2} target/debug/Zed & wait diff --git a/styles/.eslintrc.js b/styles/.eslintrc.js new file mode 100644 index 0000000000..485ff73d10 --- /dev/null +++ b/styles/.eslintrc.js @@ -0,0 +1,33 @@ +module.exports = { + env: { + node: true, + }, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:import/typescript", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + }, + plugins: ["@typescript-eslint", "import"], + globals: { + module: true, + }, + settings: { + "import/parsers": { + "@typescript-eslint/parser": [".ts"], + }, + "import/resolver": { + typescript: true, + node: true, + }, + "import/extensions": [".ts"], + }, + rules: { + "linebreak-style": ["error", "unix"], + semi: ["error", "never"], + }, +} diff --git a/styles/.gitignore b/styles/.gitignore index c2658d7d1b..25fbf5a1c4 100644 --- a/styles/.gitignore +++ b/styles/.gitignore @@ -1 +1,2 @@ node_modules/ +coverage/ diff --git a/styles/.prettierrc b/styles/.prettierrc new file mode 100644 index 0000000000..b83ccdda6a --- /dev/null +++ b/styles/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": false, + "printWidth": 80, + "htmlWhitespaceSensitivity": "strict", + "tabWidth": 4 +} diff --git a/styles/.zed/settings.json b/styles/.zed/settings.json new file mode 100644 index 0000000000..5c31fc5ac1 --- /dev/null +++ b/styles/.zed/settings.json @@ -0,0 +1,20 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://docs.zed.dev/configuration/configuring-zed#folder-specific-settings +{ + "languages": { + "TypeScript": { + "tab_size": 4 + }, + "TSX": { + "tab_size": 4 + }, + "JavaScript": { + "tab_size": 4 + }, + "JSON": { + "tab_size": 4 + } + } +} diff --git a/styles/package-lock.json b/styles/package-lock.json index d1d0ed0eb8..6fc5f746e5 100644 --- a/styles/package-lock.json +++ b/styles/package-lock.json @@ -1,7 +1,7 @@ { "name": "styles", "version": "1.0.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -12,15 +12,67 @@ "@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", - "bezier-easing": "^2.1.0", - "case-anything": "^2.1.10", "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-node": "^10.9.1" + "ts-deepmerge": "^6.0.3", + "ts-node": "^10.9.1", + "typescript": "^5.1.5", + "utility-types": "^3.10.0", + "vitest": "^0.32.0", + "zustand": "^4.3.8" } }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@bcherny/json-schema-ref-parser": { + "version": "10.0.5-fork", + "resolved": "https://registry.npmjs.org/@bcherny/json-schema-ref-parser/-/json-schema-ref-parser-10.0.5-fork.tgz", + "integrity": "sha512-E/jKbPoca1tfUPj3iSbitDZTGnq6FUFjkH6L8U2oDwSuwK1WhnnVtCG7oFOTg/DDnyoXbQYUiUiGOibHqaGVnw==", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -32,6 +84,124 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.43.0.tgz", + "integrity": "sha512-s2UHCoiXfxMvmfzqoN+vrQ84ahUSYde9qNO1MdxmoEhyHWsfmwOpFlwYV+ePJEVc7gFnATGUi376WowX1N7tFg==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==" + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", @@ -40,6 +210,14 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -54,6 +232,67 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.1.tgz", + "integrity": "sha512-JOqwkgFEyi+OROIyq7l4Jy28h/WwhDnG/cPkXG2Z1iFbubB6jsHW1NDvmyOzTBxHr3yg68YGirmh1JUgMqa+9w==", + "dependencies": { + "cross-spawn": "^7.0.3", + "fast-glob": "^3.2.12", + "is-glob": "^4.0.3", + "open": "^9.1.0", + "picocolors": "^1.0.0", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@pkgr/utils/node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + }, "node_modules/@tokens-studio/types": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.2.3.tgz", @@ -79,16 +318,361 @@ "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==" }, + "node_modules/@types/chai": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.5.tgz", + "integrity": "sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==" + }, + "node_modules/@types/chai-subset": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/chai-subset/-/chai-subset-1.3.3.tgz", + "integrity": "sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==", + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/chroma-js": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz", "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==" }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==" + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" + }, + "node_modules/@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + }, "node_modules/@types/node": { "version": "18.14.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz", "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==" }, + "node_modules/@types/prettier": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.3.tgz", + "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==" + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz", + "integrity": "sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==", + "dependencies": { + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/type-utils": "5.60.1", + "@typescript-eslint/utils": "5.60.1", + "debug": "^4.3.4", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz", + "integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==", + "dependencies": { + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz", + "integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==", + "dependencies": { + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz", + "integrity": "sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==", + "dependencies": { + "@typescript-eslint/typescript-estree": "5.60.1", + "@typescript-eslint/utils": "5.60.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz", + "integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz", + "integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==", + "dependencies": { + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.60.1.tgz", + "integrity": "sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.60.1", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/typescript-estree": "5.60.1", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz", + "integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==", + "dependencies": { + "@typescript-eslint/types": "5.60.1", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.32.0.tgz", + "integrity": "sha512-VXXlWq9X/NbsoP/l/CHLBjutsFFww1UY1qEhzGjn/DY7Tqe+z0Nu8XKc8im/XUAmjiWsh2XV7sy/F0IKAl4eaw==", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.1.5", + "magic-string": "^0.30.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2", + "test-exclude": "^6.0.0", + "v8-to-istanbul": "^9.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": ">=0.32.0 <1" + } + }, + "node_modules/@vitest/expect": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-0.32.0.tgz", + "integrity": "sha512-VxVHhIxKw9Lux+O9bwLEEk2gzOUe93xuFHy9SzYWnnoYZFYg1NfBtnfnYWiJN7yooJ7KNElCK5YtA7DTZvtXtg==", + "dependencies": { + "@vitest/spy": "0.32.0", + "@vitest/utils": "0.32.0", + "chai": "^4.3.7" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-0.32.0.tgz", + "integrity": "sha512-QpCmRxftHkr72xt5A08xTEs9I4iWEXIOCHWhQQguWOKE4QH7DXSKZSOFibuwEIMAD7G0ERvtUyQn7iPWIqSwmw==", + "dependencies": { + "@vitest/utils": "0.32.0", + "concordance": "^5.0.4", + "p-limit": "^4.0.0", + "pathe": "^1.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-0.32.0.tgz", + "integrity": "sha512-yCKorPWjEnzpUxQpGlxulujTcSPgkblwGzAUEL+z01FTUg/YuCDZ8dxr9sHA08oO2EwxzHXNLjQKWJ2zc2a19Q==", + "dependencies": { + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-0.32.0.tgz", + "integrity": "sha512-MruAPlM0uyiq3d53BkwTeShXY0rYEfhNGQzVO5GHBmmX3clsxcWp79mMnkOVcV244sNTeDcHbcPFWIjOI4tZvw==", + "dependencies": { + "tinyspy": "^2.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-0.32.0.tgz", + "integrity": "sha512-53yXunzx47MmbuvcOPpLaVljHaeSu1G2dHdmy7+9ngMnQIkBQcvwOcoclWFnxDMxFbnq8exAfh3aKSZaK71J5A==", + "dependencies": { + "concordance": "^5.0.4", + "loupe": "^2.3.6", + "pretty-format": "^27.5.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", @@ -100,6 +684,14 @@ "node": ">=0.4.0" } }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, "node_modules/acorn-walk": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", @@ -108,11 +700,146 @@ "node": ">=0.4.0" } }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", + "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "dependencies": { + "call-bind": "^1.0.2", + "is-array-buffer": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz", + "integrity": "sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "get-intrinsic": "^1.1.3", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz", + "integrity": "sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz", + "integrity": "sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ayu": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz", @@ -123,20 +850,154 @@ "nonenumerable": "^1.1.1" } }, - "node_modules/bezier-easing": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", - "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==" + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/case-anything": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", - "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==", + "node_modules/big-integer": { + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "engines": { - "node": ">=12.13" + "node": ">=0.6" + } + }, + "node_modules/blueimp-md5": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", + "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^4.1.2", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "engines": { + "node": "*" } }, "node_modules/chroma-js": { @@ -144,11 +1005,135 @@ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, + "node_modules/cli-color": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", + "integrity": "sha512-OkoZnxyC4ERN3zLzZaY9Emb7f/MhBOIpePv0Ycok0fJYT+Ouo00UBEIwsVsr0yoow++n5YWlSUgST9GKhNHiRQ==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.61", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concordance": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz", + "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==", + "dependencies": { + "date-time": "^3.1.0", + "esutils": "^2.0.3", + "fast-diff": "^1.2.0", + "js-string-escape": "^1.0.1", + "lodash": "^4.17.15", + "md5-hex": "^3.0.1", + "semver": "^7.3.2", + "well-known-symbols": "^2.0.0" + }, + "engines": { + "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dependencies": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "node_modules/date-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz", + "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==", + "dependencies": { + "time-zone": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", + "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + }, "node_modules/deepmerge": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", @@ -157,6 +1142,64 @@ "node": ">=0.10.0" } }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz", + "integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -165,21 +1208,2687 @@ "node": ">=0.3.1" } }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.21.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz", + "integrity": "sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg==", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-set-tostringtag": "^2.0.1", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.2.0", + "get-symbol-description": "^1.0.0", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.5", + "is-array-buffer": "^3.0.2", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.10", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trim": "^1.2.7", + "string.prototype.trimend": "^1.0.6", + "string.prototype.trimstart": "^1.0.6", + "typed-array-length": "^1.0.4", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", + "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", + "dependencies": { + "get-intrinsic": "^1.1.3", + "has": "^1.0.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "dependencies": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.43.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.43.0.tgz", + "integrity": "sha512-aaCpf2JqqKesMFGgmRPessmVKjcGXqdlAYLLC3THM8t5nBRZRQ+st5WM/hoJXkdioEXLLbXgclUpM0TXo5HX5Q==", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.4.0", + "@eslint/eslintrc": "^2.0.3", + "@eslint/js": "8.43.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.0", + "eslint-visitor-keys": "^3.4.1", + "espree": "^9.5.2", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz", + "integrity": "sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.11.0", + "resolve": "^1.22.1" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz", + "integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==", + "dependencies": { + "debug": "^4.3.4", + "enhanced-resolve": "^5.12.0", + "eslint-module-utils": "^2.7.4", + "get-tsconfig": "^4.5.0", + "globby": "^13.1.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "synckit": "^0.8.5" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/globby": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.0.tgz", + "integrity": "sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.27.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", + "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "array.prototype.flatmap": "^1.3.1", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.7", + "eslint-module-utils": "^2.7.4", + "has": "^1.0.3", + "is-core-module": "^2.11.0", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.6", + "resolve": "^1.22.1", + "semver": "^6.3.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz", + "integrity": "sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.2.tgz", + "integrity": "sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==", + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/execa": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.1.1.tgz", + "integrity": "sha512-wH0eMf/UXckdUYnO21+HDztteVv05rq2GXksxT4fCGeHkBhw1DROXh40wcjMcRqDOWE7iPJ4n3M7e2+YFP+76Q==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/ext/node_modules/type": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", + "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==" + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stdin": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", + "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.6.2.tgz", + "integrity": "sha512-E5XrT4CbbXcXWy+1jChlZmrmCwd5KGx502kDCXJJ7y898TtWW9FwoG5HfOLVRKmlmDGkWN2HM9Ho+/Y8F0sJDg==", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-promise": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/glob-promise/-/glob-promise-4.2.2.tgz", + "integrity": "sha512-xcUzJ8NWN5bktoTIX7eOclO1Npxd/dyVqUJxlLIDasT4C7KZyqlPIwkdJ0Ypiy3p2ZKahTjK4M9uC3sNSfNMzw==", + "dependencies": { + "@types/glob": "^7.1.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/ahmadnassri" + }, + "peerDependencies": { + "glob": "^7.1.6" + } + }, + "node_modules/globals": { + "version": "13.20.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", + "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", + "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "dependencies": { + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==" + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internal-slot": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", + "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "dependencies": { + "get-intrinsic": "^1.2.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", + "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.0", + "is-typed-array": "^1.1.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-string-escape": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", + "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==", + "engines": { + "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", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-to-typescript": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/json-schema-to-typescript/-/json-schema-to-typescript-13.0.2.tgz", + "integrity": "sha512-TCaEVW4aI2FmMQe7f98mvr3/oiVmXEC1xZjkTZ9L/BSoTXFlC7p64mD5AD2d8XWycNBQZUnHwXL5iVXt1HWwNQ==", + "dependencies": { + "@bcherny/json-schema-ref-parser": "10.0.5-fork", + "@types/json-schema": "^7.0.11", + "@types/lodash": "^4.14.182", + "@types/prettier": "^2.6.1", + "cli-color": "^2.0.2", + "get-stdin": "^8.0.0", + "glob": "^7.1.6", + "glob-promise": "^4.2.2", + "is-glob": "^4.0.3", + "lodash": "^4.17.21", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "mz": "^2.7.0", + "prettier": "^2.6.2" + }, + "bin": { + "json2ts": "dist/src/cli.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "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", + "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" }, + "node_modules/md5-hex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz", + "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==", + "dependencies": { + "blueimp-md5": "^2.10.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/memoizee": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", + "integrity": "sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.53", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.3.0.tgz", + "integrity": "sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==", + "dependencies": { + "acorn": "^8.8.2", + "pathe": "^1.1.0", + "pkg-types": "^1.0.3", + "ufo": "^1.1.2" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, "node_modules/nonenumerable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz", "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q==" }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", + "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", + "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", + "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", + "dependencies": { + "jsonc-parser": "^3.2.0", + "mlly": "^1.2.0", + "pathe": "^1.1.0" + } + }, + "node_modules/postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "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", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.1.tgz", + "integrity": "sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.2.tgz", + "integrity": "sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==" + }, + "node_modules/std-env": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.3.3.tgz", + "integrity": "sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==" + }, + "node_modules/string.prototype.trim": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz", + "integrity": "sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", + "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", + "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.20.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.0.1.tgz", + "integrity": "sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==", + "dependencies": { + "acorn": "^8.8.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", + "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", + "dependencies": { + "@pkgr/utils": "^2.3.1", + "tslib": "^2.5.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/time-zone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz", + "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/timers-ext": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz", + "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==", + "dependencies": { + "es5-ext": "~0.10.46", + "next-tick": "1" + } + }, + "node_modules/tinybench": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.0.tgz", + "integrity": "sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==" + }, + "node_modules/tinypool": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.5.0.tgz", + "integrity": "sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.1.1.tgz", + "integrity": "sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toml": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" }, + "node_modules/ts-deepmerge": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.0.3.tgz", + "integrity": "sha512-MBBJL0UK/mMnZRONMz4J1CRu5NsGtsh+gR1nkn8KLE9LXo/PCzeHhQduhNary8m5/m9ryOOyFwVKxq81cPlaow==", + "engines": { + "node": ">=14.13.1" + } + }, "node_modules/ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -222,17 +3931,145 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", + "integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", + "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", + "dependencies": { + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "is-typed-array": "^1.1.9" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.5.tgz", + "integrity": "sha512-FOH+WN/DQjUvN6WgW+c4Ml3yi0PH+a/8q+kNIfRehv1wLhWONedw85iu+vQ39Wp49IzTJEsZ2lyLXpBF7mkF1g==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.2.tgz", + "integrity": "sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==" + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "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", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "engines": { + "node": ">= 4" } }, "node_modules/v8-compile-cache-lib": { @@ -240,6 +4077,255 @@ "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" }, + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-0.32.0.tgz", + "integrity": "sha512-220P/y8YacYAU+daOAqiGEFXx2A8AwjadDzQqos6wSukjvvTWNqleJSwoUn0ckyNdjHIKoxn93Nh1vWBqEKr3Q==", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "mlly": "^1.2.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "vite": "^3.0.0 || ^4.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.32.0.tgz", + "integrity": "sha512-SW83o629gCqnV3BqBnTxhB10DAwzwEx3z+rqYZESehUB+eWsJxwcBQx7CKy0otuGMJTYh7qCVuUX23HkftGl/Q==", + "dependencies": { + "@types/chai": "^4.3.5", + "@types/chai-subset": "^1.3.3", + "@types/node": "*", + "@vitest/expect": "0.32.0", + "@vitest/runner": "0.32.0", + "@vitest/snapshot": "0.32.0", + "@vitest/spy": "0.32.0", + "@vitest/utils": "0.32.0", + "acorn": "^8.8.2", + "acorn-walk": "^8.2.0", + "cac": "^6.7.14", + "chai": "^4.3.7", + "concordance": "^5.0.4", + "debug": "^4.3.4", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.0", + "pathe": "^1.1.0", + "picocolors": "^1.0.0", + "std-env": "^3.3.2", + "strip-literal": "^1.0.1", + "tinybench": "^2.5.0", + "tinypool": "^0.5.0", + "vite": "^3.0.0 || ^4.0.0", + "vite-node": "0.32.0", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": ">=v14.18.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@vitest/browser": "*", + "@vitest/ui": "*", + "happy-dom": "*", + "jsdom": "*", + "playwright": "*", + "safaridriver": "*", + "webdriverio": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/well-known-symbols": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz", + "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", + "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -247,176 +4333,40 @@ "engines": { "node": ">=6" } - } - }, - "dependencies": { - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "requires": { - "@jridgewell/trace-mapping": "0.3.9" + }, + "node_modules/yocto-queue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", + "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" - }, - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "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 + } } - }, - "@tokens-studio/types": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@tokens-studio/types/-/types-0.2.3.tgz", - "integrity": "sha512-2KN3V0JPf+Zh8aoVMwykJq29Lsi7vYgKGYBQ/zQ+FbDEmrH6T/Vwn8kG7cvbTmW1JAAvgxVxMIivgC9PmFelNA==" - }, - "@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" - }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" - }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" - }, - "@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==" - }, - "@types/chroma-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz", - "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==" - }, - "@types/node": { - "version": "18.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.14.1.tgz", - "integrity": "sha512-QH+37Qds3E0eDlReeboBxfHbX9omAcBCXEzswCu6jySP642jiM3cYSIkU/REqwhCUqXdonHFuBfJDiAJxMNhaQ==" - }, - "acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" - }, - "acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" - }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "ayu": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz", - "integrity": "sha512-yuPZ2kZYQoYaPRQ/78F9rXDVx1rVGCJ1neBYithBoSprD6zPdIJdAKizUXG0jtTBu7nTFyAnVFFYuLnCS3cpDw==", - "requires": { - "@types/chroma-js": "^2.0.0", - "chroma-js": "^2.1.0", - "nonenumerable": "^1.1.1" - } - }, - "bezier-easing": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", - "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==" - }, - "case-anything": { - "version": "2.1.10", - "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-2.1.10.tgz", - "integrity": "sha512-JczJwVrCP0jPKh05McyVsuOg6AYosrB9XWZKbQzXeDAm2ClE/PJE/BcrrQrVyGYH7Jg8V/LDupmyL4kFlVsVFQ==" - }, - "chroma-js": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", - "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" - }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" - }, - "deepmerge": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", - "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==" - }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" - }, - "make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" - }, - "nonenumerable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz", - "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q==" - }, - "toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==" - }, - "ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - } - }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true - }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" - }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" } } } diff --git a/styles/package.json b/styles/package.json index 2a0881863b..16e95d90d5 100644 --- a/styles/package.json +++ b/styles/package.json @@ -1,31 +1,37 @@ { "name": "styles", "version": "1.0.0", - "description": "", - "main": "index.js", + "description": "Typescript app that builds Zed's themes", + "main": "./src/build_themes.ts", "scripts": { - "build": "ts-node ./src/buildThemes.ts", - "build-licenses": "ts-node ./src/buildLicenses.ts", - "build-tokens": "ts-node ./src/buildTokens.ts" + "build": "ts-node ./src/build_themes.ts", + "build-licenses": "ts-node ./src/build_licenses.ts", + "build-tokens": "ts-node ./src/build_tokens.ts", + "build-types": "ts-node ./src/build_types.ts", + "test": "vitest" }, - "author": "", + "author": "Zed Industries (https://github.com/zed-industries/)", "license": "ISC", "dependencies": { "@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", - "bezier-easing": "^2.1.0", - "case-anything": "^2.1.10", "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-node": "^10.9.1" - }, - "prettier": { - "semi": false, - "printWidth": 80, - "htmlWhitespaceSensitivity": "strict", - "tabWidth": 4 + "ts-deepmerge": "^6.0.3", + "ts-node": "^10.9.1", + "typescript": "^5.1.5", + "utility-types": "^3.10.0", + "vitest": "^0.32.0", + "zustand": "^4.3.8" } } diff --git a/styles/src/buildLicenses.ts b/styles/src/buildLicenses.ts deleted file mode 100644 index 13a6951a82..0000000000 --- a/styles/src/buildLicenses.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as fs from "fs" -import toml from "toml" -import { themes } from "./themes" -import { ThemeConfig } from "./common" - -const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml` - -// Use the cargo-about configuration file as the source of truth for supported licenses. -function parseAcceptedToml(file: string): string[] { - let buffer = fs.readFileSync(file).toString() - - let obj = toml.parse(buffer) - - if (!Array.isArray(obj.accepted)) { - throw Error("Accepted license source is malformed") - } - - return obj.accepted -} - -function checkLicenses(themes: ThemeConfig[]) { - for (const theme of themes) { - if (!theme.licenseFile) { - throw Error(`Theme ${theme.name} should have a LICENSE file`) - } - } -} - -function generateLicenseFile(themes: ThemeConfig[]) { - checkLicenses(themes) - for (const theme of themes) { - const licenseText = fs.readFileSync(theme.licenseFile).toString() - writeLicense(theme.name, licenseText, theme.licenseUrl) - } -} - -function writeLicense( - themeName: string, - licenseText: string, - licenseUrl?: string -) { - process.stdout.write( - licenseUrl - ? `## [${themeName}](${licenseUrl})\n\n${licenseText}\n********************************************************************************\n\n` - : `## ${themeName}\n\n${licenseText}\n********************************************************************************\n\n` - ) -} - -const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE) -generateLicenseFile(themes) diff --git a/styles/src/buildThemes.ts b/styles/src/buildThemes.ts deleted file mode 100644 index 8d807d62f3..0000000000 --- a/styles/src/buildThemes.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as fs from "fs" -import { tmpdir } from "os" -import * as path from "path" -import app from "./styleTree/app" -import { ColorScheme, createColorScheme } from "./theme/colorScheme" -import snakeCase from "./utils/snakeCase" -import { themes } from "./themes" - -const assetsDirectory = `${__dirname}/../../assets` -const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes")) - -// Clear existing themes -function clearThemes(themeDirectory: string) { - if (!fs.existsSync(themeDirectory)) { - fs.mkdirSync(themeDirectory, { recursive: true }) - } else { - for (const file of fs.readdirSync(themeDirectory)) { - if (file.endsWith(".json")) { - fs.unlinkSync(path.join(themeDirectory, file)) - } - } - } -} - -function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) { - clearThemes(outputDirectory) - for (let colorScheme of colorSchemes) { - let styleTree = snakeCase(app(colorScheme)) - let styleTreeJSON = JSON.stringify(styleTree, null, 2) - let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`) - let outPath = path.join(outputDirectory, `${colorScheme.name}.json`) - fs.writeFileSync(tempPath, styleTreeJSON) - fs.renameSync(tempPath, outPath) - console.log(`- ${outPath} created`) - } -} - -const colorSchemes: ColorScheme[] = themes.map((theme) => - createColorScheme(theme) -) - -// Write new themes to theme directory -writeThemes(colorSchemes, `${assetsDirectory}/themes`) diff --git a/styles/src/buildTokens.ts b/styles/src/buildTokens.ts deleted file mode 100644 index 0cf1ea037e..0000000000 --- a/styles/src/buildTokens.ts +++ /dev/null @@ -1,85 +0,0 @@ -import * as fs from "fs"; -import * as path from "path"; -import { ColorScheme, createColorScheme } from "./common"; -import { themes } from "./themes"; -import { slugify } from "./utils/slugify"; -import { colorSchemeTokens } from "./theme/tokens/colorScheme"; - -const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens"); -const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json"); -const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json"); - -function clearTokens(tokensDirectory: string) { - if (!fs.existsSync(tokensDirectory)) { - fs.mkdirSync(tokensDirectory, { recursive: true }) - } else { - for (const file of fs.readdirSync(tokensDirectory)) { - if (file.endsWith(".json")) { - fs.unlinkSync(path.join(tokensDirectory, file)) - } - } - } -} - -type TokenSet = { - id: string; - name: string; - selectedTokenSets: { [key: string]: "enabled" }; -}; - -function buildTokenSetOrder(colorSchemes: ColorScheme[]): { tokenSetOrder: string[] } { - const tokenSetOrder: string[] = colorSchemes.map( - (scheme) => scheme.name.toLowerCase().replace(/\s+/g, "_") - ); - return { tokenSetOrder }; -} - -function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] { - const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => { - const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name - .toLowerCase() - .replace(/\s+/g, "_")}_${index}`; - const selectedTokenSets: { [key: string]: "enabled" } = {}; - const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_"); - selectedTokenSets[tokenSet] = "enabled"; - - return { - id, - name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`, - selectedTokenSets, - }; - }); - - return themesIndex; -} - -function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) { - clearTokens(tokensDirectory); - - for (const colorScheme of colorSchemes) { - const fileName = slugify(colorScheme.name) + ".json"; - const tokens = colorSchemeTokens(colorScheme); - const tokensJSON = JSON.stringify(tokens, null, 2); - const outPath = path.join(tokensDirectory, fileName); - fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 }); - console.log(`- ${outPath} created`); - } - - const themeIndexData = buildThemesIndex(colorSchemes); - - const themesJSON = JSON.stringify(themeIndexData, null, 2); - fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 }); - console.log(`- ${TOKENS_FILE} created`); - - const tokenSetOrderData = buildTokenSetOrder(colorSchemes); - - const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2); - fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 }); - console.log(`- ${METADATA_FILE} created`); -} - -const colorSchemes: ColorScheme[] = themes.map((theme) => - createColorScheme(theme) -); - -writeTokens(colorSchemes, TOKENS_DIRECTORY); diff --git a/styles/src/build_licenses.ts b/styles/src/build_licenses.ts new file mode 100644 index 0000000000..76c18dfee1 --- /dev/null +++ b/styles/src/build_licenses.ts @@ -0,0 +1,50 @@ +import * as fs from "fs" +import toml from "toml" +import { themes } from "./themes" +import { ThemeConfig } from "./common" + +const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml` + +// Use the cargo-about configuration file as the source of truth for supported licenses. +function parse_accepted_toml(file: string): string[] { + const buffer = fs.readFileSync(file).toString() + + const obj = toml.parse(buffer) + + if (!Array.isArray(obj.accepted)) { + throw Error("Accepted license source is malformed") + } + + return obj.accepted +} + +function check_licenses(themes: ThemeConfig[]) { + for (const theme of themes) { + if (!theme.license_file) { + throw Error(`Theme ${theme.name} should have a LICENSE file`) + } + } +} + +function generate_license_file(themes: ThemeConfig[]) { + check_licenses(themes) + for (const theme of themes) { + const license_text = fs.readFileSync(theme.license_file).toString() + write_license(theme.name, license_text, theme.license_url) + } +} + +function write_license( + theme_name: string, + license_text: string, + license_url?: string +) { + process.stdout.write( + license_url + ? `## [${theme_name}](${license_url})\n\n${license_text}\n********************************************************************************\n\n` + : `## ${theme_name}\n\n${license_text}\n********************************************************************************\n\n` + ) +} + +const accepted_licenses = parse_accepted_toml(ACCEPTED_LICENSES_FILE) +generate_license_file(themes) diff --git a/styles/src/build_themes.ts b/styles/src/build_themes.ts new file mode 100644 index 0000000000..17575663a1 --- /dev/null +++ b/styles/src/build_themes.ts @@ -0,0 +1,47 @@ +import * as fs from "fs" +import { tmpdir } from "os" +import * as path from "path" +import app from "./style_tree/app" +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")) + +function clear_themes(theme_directory: string) { + if (!fs.existsSync(theme_directory)) { + fs.mkdirSync(theme_directory, { recursive: true }) + } else { + for (const file of fs.readdirSync(theme_directory)) { + if (file.endsWith(".json")) { + fs.unlinkSync(path.join(theme_directory, file)) + } + } + } +} + +const all_themes: Theme[] = themes.map((theme) => + create_theme(theme) +) + +function write_themes(themes: Theme[], output_directory: string) { + clear_themes(output_directory) + 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, `${theme.name}.json`) + const out_path = path.join( + output_directory, + `${theme.name}.json` + ) + fs.writeFileSync(temp_path, style_tree_json) + fs.renameSync(temp_path, out_path) + console.log(`- ${out_path} created`) + } +} + +write_themes(all_themes, `${assets_directory}/themes`) diff --git a/styles/src/build_tokens.ts b/styles/src/build_tokens.ts new file mode 100644 index 0000000000..fd6aa18ced --- /dev/null +++ b/styles/src/build_tokens.ts @@ -0,0 +1,90 @@ +import * as fs from "fs" +import * as path from "path" +import { Theme, create_theme, useThemeStore } from "./common" +import { themes } from "./themes" +import { slugify } from "./utils/slugify" +import { theme_tokens } from "./theme/tokens/theme" + +const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens") +const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json") +const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json") + +function clear_tokens(tokens_directory: string) { + if (!fs.existsSync(tokens_directory)) { + fs.mkdirSync(tokens_directory, { recursive: true }) + } else { + for (const file of fs.readdirSync(tokens_directory)) { + if (file.endsWith(".json")) { + fs.unlinkSync(path.join(tokens_directory, file)) + } + } + } +} + +type TokenSet = { + id: string + name: string + selected_token_sets: { [key: string]: "enabled" } +} + +function build_token_set_order(theme: Theme[]): { + token_set_order: string[] +} { + const token_set_order: string[] = theme.map((scheme) => + scheme.name.toLowerCase().replace(/\s+/g, "_") + ) + return { token_set_order } +} + +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() + .replace(/\s+/g, "_")}_${index}` + const selected_token_sets: { [key: string]: "enabled" } = {} + const token_set = scheme.name.toLowerCase().replace(/\s+/g, "_") + selected_token_sets[token_set] = "enabled" + + return { + id, + name: `${scheme.name} - ${scheme.is_light ? "Light" : "Dark"}`, + selected_token_sets, + } + }) + + return themes_index +} + +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() + 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 }) + console.log(`- ${out_path} created`) + } + + const theme_index_data = build_themes_index(themes) + + const themes_json = JSON.stringify(theme_index_data, null, 2) + fs.writeFileSync(TOKENS_FILE, themes_json, { mode: 0o644 }) + console.log(`- ${TOKENS_FILE} created`) + + const token_set_order_data = build_token_set_order(themes) + + const metadata_json = JSON.stringify(token_set_order_data, null, 2) + fs.writeFileSync(METADATA_FILE, metadata_json, { mode: 0o644 }) + console.log(`- ${METADATA_FILE} created`) +} + +const all_themes: Theme[] = themes.map((theme) => + create_theme(theme) +) + +write_tokens(all_themes, TOKENS_DIRECTORY) diff --git a/styles/src/build_types.ts b/styles/src/build_types.ts new file mode 100644 index 0000000000..5d7aa6e0ad --- /dev/null +++ b/styles/src/build_types.ts @@ -0,0 +1,62 @@ +import * as fs from "fs/promises" +import * as fsSync from "fs" +import * as path from "path" +import { compile } from "json-schema-to-typescript" + +const BANNER = `/* +* This file is autogenerated +*/\n\n` +const dirname = __dirname + +async function main() { + const schemas_path = path.join(dirname, "../../", "crates/theme/schemas") + const schema_files = (await fs.readdir(schemas_path)).filter((x) => + x.endsWith(".json") + ) + + const compiled_types = new Set() + + for (const filename of schema_files) { + const file_path = path.join(schemas_path, filename) + const file_contents = await fs.readFile(file_path) + const schema = JSON.parse(file_contents.toString()) + const compiled = await compile(schema, schema.title, { + bannerComment: "", + }) + const each_type = compiled.split("export") + for (const type of each_type) { + if (!type) { + continue + } + compiled_types.add("export " + type.trim()) + } + } + + const output = BANNER + Array.from(compiled_types).join("\n\n") + const output_path = path.join(dirname, "../../styles/src/types/zed.ts") + + try { + const existing = await fs.readFile(output_path) + if (existing.toString() == output) { + // Skip writing if it hasn't changed + console.log("Schemas are up to date") + return + } + } catch (e) { + if (e.code !== "ENOENT") { + throw e + } + } + + const types_dic = path.dirname(output_path) + if (!fsSync.existsSync(types_dic)) { + await fs.mkdir(types_dic) + } + await fs.writeFile(output_path, output) + console.log(`Wrote Typescript types to ${output_path}`) +} + +main().catch((e) => { + console.error(e) + process.exit(1) +}) diff --git a/styles/src/common.ts b/styles/src/common.ts index ee47bcc6bd..054b283791 100644 --- a/styles/src/common.ts +++ b/styles/src/common.ts @@ -2,42 +2,24 @@ import chroma from "chroma-js" export * from "./theme" export { chroma } -export const fontFamilies = { +export const font_families = { sans: "Zed Sans", mono: "Zed Mono", } -export const fontSizes = { - "3xs": 8, +export const font_sizes = { "2xs": 10, xs: 12, sm: 14, md: 16, lg: 18, - xl: 20, } -export type FontWeight = - | "thin" - | "extra_light" - | "light" - | "normal" - | "medium" - | "semibold" - | "bold" - | "extra_bold" - | "black" +export type FontWeight = "normal" | "bold" -export const fontWeights: { [key: string]: FontWeight } = { - thin: "thin", - extra_light: "extra_light", - light: "light", +export const font_weights: { [key: string]: FontWeight } = { normal: "normal", - medium: "medium", - semibold: "semibold", bold: "bold", - extra_bold: "extra_bold", - black: "black", } export const sizes = { diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts new file mode 100644 index 0000000000..6887fc7c30 --- /dev/null +++ b/styles/src/component/icon_button.ts @@ -0,0 +1,85 @@ +import { interactive, toggleable } from "../element" +import { background, foreground } from "../style_tree/components" +import { useTheme, Theme } from "../theme" + +export type Margin = { + top: number + bottom: number + left: number + right: number +} + +interface IconButtonOptions { + layer?: + | Theme["lowest"] + | Theme["middle"] + | Theme["highest"] + color?: keyof Theme["lowest"] + margin?: Partial +} + +type ToggleableIconButtonOptions = IconButtonOptions & { + active_color?: keyof Theme["lowest"] +} + +export function icon_button({ color, margin, layer }: IconButtonOptions) { + const theme = useTheme() + + if (!color) color = "base" + + const m = { + top: margin?.top ?? 0, + bottom: margin?.bottom ?? 0, + left: margin?.left ?? 0, + right: margin?.right ?? 0, + } + + return interactive({ + base: { + corner_radius: 6, + padding: { + top: 2, + bottom: 2, + left: 4, + right: 4, + }, + margin: m, + icon_width: 14, + icon_height: 14, + button_width: 20, + button_height: 16, + }, + state: { + default: { + background: background(layer ?? theme.lowest, color), + color: foreground(layer ?? theme.lowest, color), + }, + hovered: { + background: background(layer ?? theme.lowest, color, "hovered"), + color: foreground(layer ?? theme.lowest, color, "hovered"), + }, + clicked: { + background: background(layer ?? theme.lowest, color, "pressed"), + color: foreground(layer ?? theme.lowest, color, "pressed"), + }, + }, + }) +} + +export function toggleable_icon_button( + theme: Theme, + { color, active_color, margin }: ToggleableIconButtonOptions +) { + if (!color) color = "base" + + return toggleable({ + state: { + inactive: icon_button({ color, margin }), + active: icon_button({ + color: active_color ? active_color : color, + margin, + layer: theme.middle, + }), + }, + }) +} diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts new file mode 100644 index 0000000000..58b2a1cbf2 --- /dev/null +++ b/styles/src/component/text_button.ts @@ -0,0 +1,93 @@ +import { interactive, toggleable } from "../element" +import { + TextProperties, + background, + foreground, + text, +} from "../style_tree/components" +import { useTheme, Theme } from "../theme" +import { Margin } from "./icon_button" + +interface TextButtonOptions { + layer?: + | Theme["lowest"] + | Theme["middle"] + | Theme["highest"] + color?: keyof Theme["lowest"] + margin?: Partial + text_properties?: TextProperties +} + +type ToggleableTextButtonOptions = TextButtonOptions & { + active_color?: keyof Theme["lowest"] +} + +export function text_button({ + color, + layer, + margin, + text_properties, +}: TextButtonOptions) { + const theme = useTheme() + if (!color) color = "base" + + const text_options: TextProperties = { + size: "xs", + weight: "normal", + ...text_properties, + } + + const m = { + top: margin?.top ?? 0, + bottom: margin?.bottom ?? 0, + left: margin?.left ?? 0, + right: margin?.right ?? 0, + } + + return interactive({ + base: { + corner_radius: 6, + padding: { + top: 1, + bottom: 1, + left: 6, + right: 6, + }, + margin: m, + button_height: 22, + ...text(layer ?? theme.lowest, "sans", color, text_options), + }, + state: { + default: { + background: background(layer ?? theme.lowest, color), + color: foreground(layer ?? theme.lowest, color), + }, + hovered: { + background: background(layer ?? theme.lowest, color, "hovered"), + color: foreground(layer ?? theme.lowest, color, "hovered"), + }, + clicked: { + background: background(layer ?? theme.lowest, color, "pressed"), + color: foreground(layer ?? theme.lowest, color, "pressed"), + }, + }, + }) +} + +export function toggleable_text_button( + theme: Theme, + { color, active_color, margin }: ToggleableTextButtonOptions +) { + if (!color) color = "base" + + return toggleable({ + state: { + inactive: text_button({ color, margin }), + active: text_button({ + color: active_color ? active_color : color, + margin, + layer: theme.middle, + }), + }, + }) +} diff --git a/styles/src/element/index.ts b/styles/src/element/index.ts new file mode 100644 index 0000000000..81c911c7bd --- /dev/null +++ b/styles/src/element/index.ts @@ -0,0 +1,4 @@ +import { interactive, Interactive } from "./interactive" +import { toggleable } from "./toggle" + +export { interactive, Interactive, toggleable } diff --git a/styles/src/element/interactive.test.ts b/styles/src/element/interactive.test.ts new file mode 100644 index 0000000000..0e0013fc07 --- /dev/null +++ b/styles/src/element/interactive.test.ts @@ -0,0 +1,56 @@ +import { + NOT_ENOUGH_STATES_ERROR, + NO_DEFAULT_OR_BASE_ERROR, + interactive, +} from "./interactive" +import { describe, it, expect } from "vitest" + +describe("interactive", () => { + it("creates an Interactive with base properties and states", () => { + const result = interactive({ + base: { font_size: 10, color: "#FFFFFF" }, + state: { + hovered: { color: "#EEEEEE" }, + clicked: { color: "#CCCCCC" }, + }, + }) + + expect(result).toEqual({ + default: { color: "#FFFFFF", font_size: 10 }, + hovered: { color: "#EEEEEE", font_size: 10 }, + clicked: { color: "#CCCCCC", font_size: 10 }, + }) + }) + + it("creates an Interactive with no base properties", () => { + const result = interactive({ + state: { + default: { color: "#FFFFFF", font_size: 10 }, + hovered: { color: "#EEEEEE" }, + clicked: { color: "#CCCCCC" }, + }, + }) + + expect(result).toEqual({ + default: { color: "#FFFFFF", font_size: 10 }, + hovered: { color: "#EEEEEE", font_size: 10 }, + clicked: { color: "#CCCCCC", font_size: 10 }, + }) + }) + + it("throws error when both default and base are missing", () => { + const state = { + hovered: { color: "blue" }, + } + + expect(() => interactive({ state })).toThrow(NO_DEFAULT_OR_BASE_ERROR) + }) + + it("throws error when no other state besides default is present", () => { + const state = { + default: { font_size: 10 }, + } + + expect(() => interactive({ state })).toThrow(NOT_ENOUGH_STATES_ERROR) + }) +}) diff --git a/styles/src/element/interactive.ts b/styles/src/element/interactive.ts new file mode 100644 index 0000000000..59ccff40f7 --- /dev/null +++ b/styles/src/element/interactive.ts @@ -0,0 +1,97 @@ +import merge from "ts-deepmerge" +import { DeepPartial } from "utility-types" + +export type InteractiveState = + | "default" + | "hovered" + | "clicked" + | "selected" + | "disabled" + +export type Interactive = { + default: T + hovered?: T + clicked?: T + selected?: T + disabled?: T +} + +export const NO_DEFAULT_OR_BASE_ERROR = + "An interactive object must have a default state, or a base property." +export const NOT_ENOUGH_STATES_ERROR = + "An interactive object must have a default and at least one other state." + +interface InteractiveProps { + base?: T + state: Partial>> +} + +/** + * Helper function for creating Interactive objects that works with Toggle-like behavior. + * It takes a default object to be used as the value for `default` field and fills out other fields + * with fields from either `base` or from the `state` object which contains values for specific states. + * Notably, it does not touch `hover`, `clicked`, `selected` and `disabled` states if there are no modifications for them. + * + * @param defaultObj Object to be used as the value for the `default` field. + * @param base Optional object containing base fields to be included in the resulting object. + * @param state Object containing optional modified fields to be included in the resulting object for each state. + * @returns Interactive object with fields from `base` and `state`. + */ +export function interactive({ + base, + state, +}: InteractiveProps): Interactive { + if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR) + + let default_state: T + + if (state.default && base) { + default_state = merge(base, state.default) as T + } else { + default_state = base ? base : (state.default as T) + } + + const interactive_obj: Interactive = { + default: default_state, + } + + let state_count = 0 + + if (state.hovered !== undefined) { + interactive_obj.hovered = merge( + interactive_obj.default, + state.hovered + ) as T + state_count++ + } + + if (state.clicked !== undefined) { + interactive_obj.clicked = merge( + interactive_obj.default, + state.clicked + ) as T + state_count++ + } + + if (state.selected !== undefined) { + interactive_obj.selected = merge( + interactive_obj.default, + state.selected + ) as T + state_count++ + } + + if (state.disabled !== undefined) { + interactive_obj.disabled = merge( + interactive_obj.default, + state.disabled + ) as T + state_count++ + } + + if (state_count < 1) { + throw new Error(NOT_ENOUGH_STATES_ERROR) + } + + return interactive_obj +} diff --git a/styles/src/element/toggle.test.ts b/styles/src/element/toggle.test.ts new file mode 100644 index 0000000000..8018ce1039 --- /dev/null +++ b/styles/src/element/toggle.test.ts @@ -0,0 +1,52 @@ +import { + NO_ACTIVE_ERROR, + NO_INACTIVE_OR_BASE_ERROR, + toggleable, +} from "./toggle" +import { describe, it, expect } from "vitest" + +describe("toggleable", () => { + it("creates a Toggleable with base properties and states", () => { + const result = toggleable({ + base: { background: "#000000", color: "#CCCCCC" }, + state: { + active: { color: "#FFFFFF" }, + }, + }) + + expect(result).toEqual({ + inactive: { background: "#000000", color: "#CCCCCC" }, + active: { background: "#000000", color: "#FFFFFF" }, + }) + }) + + it("creates a Toggleable with no base properties", () => { + const result = toggleable({ + state: { + inactive: { background: "#000000", color: "#CCCCCC" }, + active: { background: "#000000", color: "#FFFFFF" }, + }, + }) + + expect(result).toEqual({ + inactive: { background: "#000000", color: "#CCCCCC" }, + active: { background: "#000000", color: "#FFFFFF" }, + }) + }) + + it("throws error when both inactive and base are missing", () => { + const state = { + active: { background: "#000000", color: "#FFFFFF" }, + } + + expect(() => toggleable({ state })).toThrow(NO_INACTIVE_OR_BASE_ERROR) + }) + + it("throws error when no active state is present", () => { + const state = { + inactive: { background: "#000000", color: "#CCCCCC" }, + } + + expect(() => toggleable({ state })).toThrow(NO_ACTIVE_ERROR) + }) +}) diff --git a/styles/src/element/toggle.ts b/styles/src/element/toggle.ts new file mode 100644 index 0000000000..c3cde46d65 --- /dev/null +++ b/styles/src/element/toggle.ts @@ -0,0 +1,47 @@ +import merge from "ts-deepmerge" +import { DeepPartial } from "utility-types" + +type ToggleState = "inactive" | "active" + +type Toggleable = Record + +export const NO_INACTIVE_OR_BASE_ERROR = + "A toggleable object must have an inactive state, or a base property." +export const NO_ACTIVE_ERROR = "A toggleable object must have an active state." + +interface ToggleableProps { + base?: T + state: Partial>> +} + +/** + * Helper function for creating Toggleable objects. + * @template T The type of the object being toggled. + * @param props Object containing the base (inactive) state and state modifications to create the active state. + * @returns A Toggleable object containing both the inactive and active states. + * @example + * ``` + * toggleable({ + * base: { background: "#000000", text: "#CCCCCC" }, + * state: { active: { text: "#CCCCCC" } }, + * }) + * ``` + */ +export function toggleable( + props: ToggleableProps +): Toggleable { + const { base, state } = props + + if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR) + if (!state.active) throw new Error(NO_ACTIVE_ERROR) + + const inactive_state = base + ? ((state.inactive ? merge(base, state.inactive) : base) as T) + : (state.inactive as T) + + const toggle_obj: Toggleable = { + inactive: inactive_state, + active: merge(base ?? {}, state.active) as T, + } + return toggle_obj +} diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts deleted file mode 100644 index 6244cbae10..0000000000 --- a/styles/src/styleTree/app.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { text } from "./components" -import contactFinder from "./contactFinder" -import contactsPopover from "./contactsPopover" -import commandPalette from "./commandPalette" -import editor from "./editor" -import projectPanel from "./projectPanel" -import search from "./search" -import picker from "./picker" -import workspace from "./workspace" -import contextMenu from "./contextMenu" -import sharedScreen from "./sharedScreen" -import projectDiagnostics from "./projectDiagnostics" -import contactNotification from "./contactNotification" -import updateNotification from "./updateNotification" -import simpleMessageNotification from "./simpleMessageNotification" -import projectSharedNotification from "./projectSharedNotification" -import tooltip from "./tooltip" -import terminal from "./terminal" -import contactList from "./contactList" -import toolbarDropdownMenu from "./toolbarDropdownMenu" -import incomingCallNotification from "./incomingCallNotification" -import { ColorScheme } from "../theme/colorScheme" -import feedback from "./feedback" -import welcome from "./welcome" -import copilot from "./copilot" -import assistant from "./assistant" - -export default function app(colorScheme: ColorScheme): Object { - return { - meta: { - name: colorScheme.name, - isLight: colorScheme.isLight, - }, - commandPalette: commandPalette(colorScheme), - contactNotification: contactNotification(colorScheme), - projectSharedNotification: projectSharedNotification(colorScheme), - incomingCallNotification: incomingCallNotification(colorScheme), - picker: picker(colorScheme), - workspace: workspace(colorScheme), - copilot: copilot(colorScheme), - welcome: welcome(colorScheme), - contextMenu: contextMenu(colorScheme), - editor: editor(colorScheme), - projectDiagnostics: projectDiagnostics(colorScheme), - projectPanel: projectPanel(colorScheme), - contactsPopover: contactsPopover(colorScheme), - contactFinder: contactFinder(colorScheme), - contactList: contactList(colorScheme), - toolbarDropdownMenu: toolbarDropdownMenu(colorScheme), - search: search(colorScheme), - sharedScreen: sharedScreen(colorScheme), - updateNotification: updateNotification(colorScheme), - simpleMessageNotification: simpleMessageNotification(colorScheme), - tooltip: tooltip(colorScheme), - terminal: terminal(colorScheme), - assistant: assistant(colorScheme), - feedback: feedback(colorScheme), - colorScheme: { - ...colorScheme, - players: Object.values(colorScheme.players), - ramps: { - neutral: colorScheme.ramps.neutral.colors(100, "hex"), - red: colorScheme.ramps.red.colors(100, "hex"), - orange: colorScheme.ramps.orange.colors(100, "hex"), - yellow: colorScheme.ramps.yellow.colors(100, "hex"), - green: colorScheme.ramps.green.colors(100, "hex"), - cyan: colorScheme.ramps.cyan.colors(100, "hex"), - blue: colorScheme.ramps.blue.colors(100, "hex"), - violet: colorScheme.ramps.violet.colors(100, "hex"), - magenta: colorScheme.ramps.magenta.colors(100, "hex"), - }, - }, - } -} diff --git a/styles/src/styleTree/assistant.ts b/styles/src/styleTree/assistant.ts deleted file mode 100644 index bbb4aae5e1..0000000000 --- a/styles/src/styleTree/assistant.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { text, border, background, foreground } from "./components" -import editor from "./editor" - -export default function assistant(colorScheme: ColorScheme) { - const layer = colorScheme.highest - return { - container: { - background: editor(colorScheme).background, - padding: { left: 12 }, - }, - header: { - border: border(layer, "default", { bottom: true, top: true }), - margin: { bottom: 6, top: 6 }, - background: editor(colorScheme).background, - }, - userSender: { - ...text(layer, "sans", "default", { size: "sm", weight: "bold" }), - }, - assistantSender: { - ...text(layer, "sans", "accent", { size: "sm", weight: "bold" }), - }, - systemSender: { - ...text(layer, "sans", "variant", { size: "sm", weight: "bold" }), - }, - sentAt: { - margin: { top: 2, left: 8 }, - ...text(layer, "sans", "default", { size: "2xs" }), - }, - modelInfoContainer: { - margin: { right: 16, top: 4 }, - }, - model: { - background: background(layer, "on"), - border: border(layer, "on", { overlay: true }), - padding: 4, - cornerRadius: 4, - ...text(layer, "sans", "default", { size: "xs" }), - hover: { - background: background(layer, "on", "hovered"), - }, - }, - remainingTokens: { - background: background(layer, "on"), - border: border(layer, "on", { overlay: true }), - padding: 4, - margin: { left: 4 }, - cornerRadius: 4, - ...text(layer, "sans", "positive", { size: "xs" }), - }, - noRemainingTokens: { - background: background(layer, "on"), - border: border(layer, "on", { overlay: true }), - padding: 4, - margin: { left: 4 }, - cornerRadius: 4, - ...text(layer, "sans", "negative", { size: "xs" }), - }, - errorIcon: { - margin: { left: 8 }, - color: foreground(layer, "negative"), - width: 12, - }, - apiKeyEditor: { - background: background(layer, "on"), - cornerRadius: 6, - text: text(layer, "mono", "on"), - placeholderText: text(layer, "mono", "on", "disabled", { - size: "xs", - }), - selection: colorScheme.players[0], - border: border(layer, "on"), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - }, - apiKeyPrompt: { - padding: 10, - ...text(layer, "sans", "default", { size: "xs" }), - }, - } -} diff --git a/styles/src/styleTree/commandPalette.ts b/styles/src/styleTree/commandPalette.ts deleted file mode 100644 index c49e1f194c..0000000000 --- a/styles/src/styleTree/commandPalette.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { withOpacity } from "../theme/color" -import { text, background } from "./components" - -export default function commandPalette(colorScheme: ColorScheme) { - let layer = colorScheme.highest - return { - keystrokeSpacing: 8, - key: { - text: text(layer, "mono", "variant", "default", { size: "xs" }), - cornerRadius: 2, - background: background(layer, "on"), - padding: { - top: 1, - bottom: 1, - left: 6, - right: 6, - }, - margin: { - top: 1, - bottom: 1, - left: 2, - }, - active: { - text: text(layer, "mono", "on", "default", { size: "xs" }), - background: withOpacity(background(layer, "on"), 0.2), - }, - }, - } -} diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts deleted file mode 100644 index e45647c3d6..0000000000 --- a/styles/src/styleTree/contactFinder.ts +++ /dev/null @@ -1,70 +0,0 @@ -import picker from "./picker" -import { ColorScheme } from "../theme/colorScheme" -import { background, border, foreground, text } from "./components" - -export default function contactFinder(colorScheme: ColorScheme): any { - let layer = colorScheme.middle - - const sideMargin = 6 - const contactButton = { - background: background(layer, "variant"), - color: foreground(layer, "variant"), - iconWidth: 8, - buttonWidth: 16, - cornerRadius: 8, - } - - const pickerStyle = picker(colorScheme) - const pickerInput = { - background: background(layer, "on"), - cornerRadius: 6, - text: text(layer, "mono"), - placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }), - selection: colorScheme.players[0], - border: border(layer), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - margin: { - left: sideMargin, - right: sideMargin, - }, - } - - return { - picker: { - emptyContainer: {}, - item: { - ...pickerStyle.item, - margin: { left: sideMargin, right: sideMargin }, - }, - noMatches: pickerStyle.noMatches, - inputEditor: pickerInput, - emptyInputEditor: pickerInput, - }, - rowHeight: 28, - contactAvatar: { - cornerRadius: 10, - width: 18, - }, - contactUsername: { - padding: { - left: 8, - }, - }, - contactButton: { - ...contactButton, - hover: { - background: background(layer, "variant", "hovered"), - }, - }, - disabledContactButton: { - ...contactButton, - background: background(layer, "disabled"), - color: foreground(layer, "disabled"), - }, - } -} diff --git a/styles/src/styleTree/contactList.ts b/styles/src/styleTree/contactList.ts deleted file mode 100644 index a597e44d9f..0000000000 --- a/styles/src/styleTree/contactList.ts +++ /dev/null @@ -1,182 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, borderColor, foreground, text } from "./components" - -export default function contactsPanel(colorScheme: ColorScheme) { - const nameMargin = 8 - const sidePadding = 12 - - let layer = colorScheme.middle - - const contactButton = { - background: background(layer, "on"), - color: foreground(layer, "on"), - iconWidth: 8, - buttonWidth: 16, - cornerRadius: 8, - } - const projectRow = { - guestAvatarSpacing: 4, - height: 24, - guestAvatar: { - cornerRadius: 8, - width: 14, - }, - name: { - ...text(layer, "mono", { size: "sm" }), - margin: { - left: nameMargin, - right: 6, - }, - }, - guests: { - margin: { - left: nameMargin, - right: nameMargin, - }, - }, - padding: { - left: sidePadding, - right: sidePadding, - }, - } - - return { - background: background(layer), - padding: { top: 12 }, - userQueryEditor: { - background: background(layer, "on"), - cornerRadius: 6, - text: text(layer, "mono", "on"), - placeholderText: text(layer, "mono", "on", "disabled", { - size: "xs", - }), - selection: colorScheme.players[0], - border: border(layer, "on"), - padding: { - bottom: 4, - left: 8, - right: 8, - top: 4, - }, - margin: { - left: 6, - }, - }, - userQueryEditorHeight: 33, - addContactButton: { - margin: { left: 6, right: 12 }, - color: foreground(layer, "on"), - buttonWidth: 28, - iconWidth: 16, - }, - rowHeight: 28, - sectionIconSize: 8, - headerRow: { - ...text(layer, "mono", { size: "sm" }), - margin: { top: 14 }, - padding: { - left: sidePadding, - right: sidePadding, - }, - active: { - ...text(layer, "mono", "active", { size: "sm" }), - background: background(layer, "active"), - }, - }, - leaveCall: { - background: background(layer), - border: border(layer), - cornerRadius: 6, - margin: { - top: 1, - }, - padding: { - top: 1, - bottom: 1, - left: 7, - right: 7, - }, - ...text(layer, "sans", "variant", { size: "xs" }), - hover: { - ...text(layer, "sans", "hovered", { size: "xs" }), - background: background(layer, "hovered"), - border: border(layer, "hovered"), - }, - }, - contactRow: { - padding: { - left: sidePadding, - right: sidePadding, - }, - active: { - background: background(layer, "active"), - }, - }, - contactAvatar: { - cornerRadius: 10, - width: 18, - }, - contactStatusFree: { - cornerRadius: 4, - padding: 4, - margin: { top: 12, left: 12 }, - background: foreground(layer, "positive"), - }, - contactStatusBusy: { - cornerRadius: 4, - padding: 4, - margin: { top: 12, left: 12 }, - background: foreground(layer, "negative"), - }, - contactUsername: { - ...text(layer, "mono", { size: "sm" }), - margin: { - left: nameMargin, - }, - }, - contactButtonSpacing: nameMargin, - contactButton: { - ...contactButton, - hover: { - background: background(layer, "hovered"), - }, - }, - disabledButton: { - ...contactButton, - background: background(layer, "on"), - color: foreground(layer, "on"), - }, - callingIndicator: { - ...text(layer, "mono", "variant", { size: "xs" }), - }, - treeBranch: { - color: borderColor(layer), - width: 1, - hover: { - color: borderColor(layer), - }, - active: { - color: borderColor(layer), - }, - }, - projectRow: { - ...projectRow, - background: background(layer), - icon: { - margin: { left: nameMargin }, - color: foreground(layer, "variant"), - width: 12, - }, - name: { - ...projectRow.name, - ...text(layer, "mono", { size: "sm" }), - }, - hover: { - background: background(layer, "hovered"), - }, - active: { - background: background(layer, "active"), - }, - }, - } -} diff --git a/styles/src/styleTree/contactNotification.ts b/styles/src/styleTree/contactNotification.ts deleted file mode 100644 index 85a0b9d0de..0000000000 --- a/styles/src/styleTree/contactNotification.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, foreground, text } from "./components" - -const avatarSize = 12 -const headerPadding = 8 - -export default function contactNotification(colorScheme: ColorScheme): Object { - let layer = colorScheme.lowest - return { - headerAvatar: { - height: avatarSize, - width: avatarSize, - cornerRadius: 6, - }, - headerMessage: { - ...text(layer, "sans", { size: "xs" }), - margin: { left: headerPadding, right: headerPadding }, - }, - headerHeight: 18, - bodyMessage: { - ...text(layer, "sans", { size: "xs" }), - margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 }, - }, - button: { - ...text(layer, "sans", "on", { size: "xs" }), - background: background(layer, "on"), - padding: 4, - cornerRadius: 6, - margin: { left: 6 }, - hover: { - background: background(layer, "on", "hovered"), - }, - }, - dismissButton: { - color: foreground(layer, "variant"), - iconWidth: 8, - iconHeight: 8, - buttonWidth: 8, - buttonHeight: 8, - hover: { - color: foreground(layer, "hovered"), - }, - }, - } -} diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts deleted file mode 100644 index 5946bfb82c..0000000000 --- a/styles/src/styleTree/contactsPopover.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, text } from "./components" - -export default function contactsPopover(colorScheme: ColorScheme) { - let layer = colorScheme.middle - const sidePadding = 12 - return { - background: background(layer), - cornerRadius: 6, - padding: { top: 6, bottom: 6 }, - shadow: colorScheme.popoverShadow, - border: border(layer), - width: 300, - height: 400, - } -} diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts deleted file mode 100644 index f14cd90219..0000000000 --- a/styles/src/styleTree/contextMenu.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, borderColor, text } from "./components" - -export default function contextMenu(colorScheme: ColorScheme) { - let layer = colorScheme.middle - return { - background: background(layer), - cornerRadius: 10, - padding: 4, - shadow: colorScheme.popoverShadow, - border: border(layer), - keystrokeMargin: 30, - item: { - iconSpacing: 8, - iconWidth: 14, - padding: { left: 6, right: 6, top: 2, bottom: 2 }, - cornerRadius: 6, - label: text(layer, "sans", { size: "sm" }), - keystroke: { - ...text(layer, "sans", "variant", { - size: "sm", - weight: "bold", - }), - padding: { left: 3, right: 3 }, - }, - hover: { - background: background(layer, "hovered"), - label: text(layer, "sans", "hovered", { size: "sm" }), - keystroke: { - ...text(layer, "sans", "hovered", { - size: "sm", - weight: "bold", - }), - padding: { left: 3, right: 3 }, - }, - }, - active: { - background: background(layer, "active"), - }, - activeHover: { - background: background(layer, "active"), - }, - }, - separator: { - background: borderColor(layer), - margin: { top: 2, bottom: 2 }, - }, - } -} diff --git a/styles/src/styleTree/copilot.ts b/styles/src/styleTree/copilot.ts deleted file mode 100644 index 8614cb6976..0000000000 --- a/styles/src/styleTree/copilot.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, foreground, svg, text } from "./components" - -export default function copilot(colorScheme: ColorScheme) { - let layer = colorScheme.middle - - let content_width = 264 - - let ctaButton = { - // Copied from welcome screen. FIXME: Move this into a ZDS component - background: background(layer), - border: border(layer, "default"), - cornerRadius: 4, - margin: { - top: 4, - bottom: 4, - left: 8, - right: 8, - }, - padding: { - top: 3, - bottom: 3, - left: 7, - right: 7, - }, - ...text(layer, "sans", "default", { size: "sm" }), - hover: { - ...text(layer, "sans", "default", { size: "sm" }), - background: background(layer, "hovered"), - border: border(layer, "active"), - }, - } - - return { - outLinkIcon: { - icon: svg( - foreground(layer, "variant"), - "icons/link_out_12.svg", - 12, - 12 - ), - container: { - cornerRadius: 6, - padding: { left: 6 }, - }, - hover: { - icon: svg( - foreground(layer, "hovered"), - "icons/link_out_12.svg", - 12, - 12 - ), - }, - }, - modal: { - titleText: { - ...text(layer, "sans", { size: "xs", weight: "bold" }), - }, - titlebar: { - background: background(colorScheme.lowest), - border: border(layer, "active"), - padding: { - top: 4, - bottom: 4, - left: 8, - right: 8, - }, - }, - container: { - background: background(colorScheme.lowest), - padding: { - top: 0, - left: 0, - right: 0, - bottom: 8, - }, - }, - closeIcon: { - icon: svg( - foreground(layer, "variant"), - "icons/x_mark_8.svg", - 8, - 8 - ), - container: { - cornerRadius: 2, - padding: { - top: 4, - bottom: 4, - left: 4, - right: 4, - }, - margin: { - right: 0, - }, - }, - hover: { - icon: svg( - foreground(layer, "on"), - "icons/x_mark_8.svg", - 8, - 8 - ), - }, - clicked: { - icon: svg( - foreground(layer, "base"), - "icons/x_mark_8.svg", - 8, - 8 - ), - }, - }, - dimensions: { - width: 280, - height: 280, - }, - }, - - auth: { - content_width, - - ctaButton, - - header: { - icon: svg( - foreground(layer, "default"), - "icons/zed_plus_copilot_32.svg", - 92, - 32 - ), - container: { - margin: { - top: 35, - bottom: 5, - left: 0, - right: 0, - }, - }, - }, - - prompting: { - subheading: { - ...text(layer, "sans", { size: "xs" }), - margin: { - top: 6, - bottom: 12, - left: 0, - right: 0, - }, - }, - - hint: { - ...text(layer, "sans", { size: "xs", color: "#838994" }), - margin: { - top: 6, - bottom: 2, - }, - }, - - deviceCode: { - text: text(layer, "mono", { size: "sm" }), - cta: { - ...ctaButton, - background: background(colorScheme.lowest), - border: border(colorScheme.lowest, "inverted"), - padding: { - top: 0, - bottom: 0, - left: 16, - right: 16, - }, - margin: { - left: 16, - right: 16, - }, - }, - left: content_width / 2, - leftContainer: { - padding: { - top: 3, - bottom: 3, - left: 0, - right: 6, - }, - }, - right: (content_width * 1) / 3, - rightContainer: { - border: border(colorScheme.lowest, "inverted", { - bottom: false, - right: false, - top: false, - left: true, - }), - padding: { - top: 3, - bottom: 5, - left: 8, - right: 0, - }, - hover: { - border: border(layer, "active", { - bottom: false, - right: false, - top: false, - left: true, - }), - }, - }, - }, - }, - - notAuthorized: { - subheading: { - ...text(layer, "sans", { size: "xs" }), - - margin: { - top: 16, - bottom: 16, - left: 0, - right: 0, - }, - }, - - warning: { - ...text(layer, "sans", { - size: "xs", - color: foreground(layer, "warning"), - }), - border: border(layer, "warning"), - background: background(layer, "warning"), - cornerRadius: 2, - padding: { - top: 4, - left: 4, - bottom: 4, - right: 4, - }, - margin: { - bottom: 16, - left: 8, - right: 8, - }, - }, - }, - - authorized: { - subheading: { - ...text(layer, "sans", { size: "xs" }), - - margin: { - top: 16, - bottom: 16, - }, - }, - - hint: { - ...text(layer, "sans", { size: "xs", color: "#838994" }), - margin: { - top: 24, - bottom: 4, - }, - }, - }, - }, - } -} diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts deleted file mode 100644 index 859f9fe1b9..0000000000 --- a/styles/src/styleTree/editor.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { withOpacity } from "../theme/color" -import { ColorScheme, Layer, StyleSets } from "../theme/colorScheme" -import { background, border, borderColor, foreground, text } from "./components" -import hoverPopover from "./hoverPopover" - -import { buildSyntax } from "../theme/syntax" - -export default function editor(colorScheme: ColorScheme) { - const { isLight } = colorScheme - - let layer = colorScheme.highest - - const autocompleteItem = { - cornerRadius: 6, - padding: { - bottom: 2, - left: 6, - right: 6, - top: 2, - }, - } - - function diagnostic(layer: Layer, styleSet: StyleSets) { - return { - textScaleFactor: 0.857, - header: { - border: border(layer, { - top: true, - }), - }, - message: { - text: text(layer, "sans", styleSet, "default", { size: "sm" }), - highlightText: text(layer, "sans", styleSet, "default", { - size: "sm", - weight: "bold", - }), - }, - } - } - - const syntax = buildSyntax(colorScheme) - - return { - textColor: syntax.primary.color, - background: background(layer), - activeLineBackground: withOpacity(background(layer, "on"), 0.75), - highlightedLineBackground: background(layer, "on"), - // Inline autocomplete suggestions, Co-pilot suggestions, etc. - suggestion: syntax.predictive, - codeActions: { - indicator: { - color: foreground(layer, "variant"), - - clicked: { - color: foreground(layer, "base"), - }, - hover: { - color: foreground(layer, "on"), - }, - active: { - color: foreground(layer, "on"), - }, - }, - verticalScale: 0.55, - }, - folds: { - iconMarginScale: 2.5, - foldedIcon: "icons/chevron_right_8.svg", - foldableIcon: "icons/chevron_down_8.svg", - indicator: { - color: foreground(layer, "variant"), - - clicked: { - color: foreground(layer, "base"), - }, - hover: { - color: foreground(layer, "on"), - }, - active: { - color: foreground(layer, "on"), - }, - }, - ellipses: { - textColor: colorScheme.ramps.neutral(0.71).hex(), - cornerRadiusFactor: 0.15, - background: { - // Copied from hover_popover highlight - color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(), - - hover: { - color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(), - }, - - clicked: { - color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(), - }, - }, - }, - foldBackground: foreground(layer, "variant"), - }, - diff: { - deleted: isLight - ? colorScheme.ramps.red(0.5).hex() - : colorScheme.ramps.red(0.4).hex(), - modified: isLight - ? colorScheme.ramps.yellow(0.5).hex() - : colorScheme.ramps.yellow(0.5).hex(), - inserted: isLight - ? colorScheme.ramps.green(0.4).hex() - : colorScheme.ramps.green(0.5).hex(), - removedWidthEm: 0.275, - widthEm: 0.15, - cornerRadius: 0.05, - }, - /** Highlights matching occurrences of what is under the cursor - * as well as matched brackets - */ - documentHighlightReadBackground: withOpacity( - foreground(layer, "accent"), - 0.1 - ), - documentHighlightWriteBackground: colorScheme.ramps - .neutral(0.5) - .alpha(0.4) - .hex(), // TODO: This was blend * 2 - errorColor: background(layer, "negative"), - gutterBackground: background(layer), - gutterPaddingFactor: 3.5, - lineNumber: withOpacity(foreground(layer), 0.35), - lineNumberActive: foreground(layer), - renameFade: 0.6, - unnecessaryCodeFade: 0.5, - selection: colorScheme.players[0], - whitespace: colorScheme.ramps.neutral(0.5).hex(), - guestSelections: [ - colorScheme.players[1], - colorScheme.players[2], - colorScheme.players[3], - colorScheme.players[4], - colorScheme.players[5], - colorScheme.players[6], - colorScheme.players[7], - ], - autocomplete: { - background: background(colorScheme.middle), - cornerRadius: 8, - padding: 4, - margin: { - left: -14, - }, - border: border(colorScheme.middle), - shadow: colorScheme.popoverShadow, - matchHighlight: foreground(colorScheme.middle, "accent"), - item: autocompleteItem, - hoveredItem: { - ...autocompleteItem, - matchHighlight: foreground( - colorScheme.middle, - "accent", - "hovered" - ), - background: background(colorScheme.middle, "hovered"), - }, - selectedItem: { - ...autocompleteItem, - matchHighlight: foreground( - colorScheme.middle, - "accent", - "active" - ), - background: background(colorScheme.middle, "active"), - }, - }, - diagnosticHeader: { - background: background(colorScheme.middle), - iconWidthFactor: 1.5, - textScaleFactor: 0.857, - border: border(colorScheme.middle, { - bottom: true, - top: true, - }), - code: { - ...text(colorScheme.middle, "mono", { size: "sm" }), - margin: { - left: 10, - }, - }, - source: { - text: text(colorScheme.middle, "sans", { - size: "sm", - weight: "bold", - }), - }, - message: { - highlightText: text(colorScheme.middle, "sans", { - size: "sm", - weight: "bold", - }), - text: text(colorScheme.middle, "sans", { size: "sm" }), - }, - }, - diagnosticPathHeader: { - background: background(colorScheme.middle), - textScaleFactor: 0.857, - filename: text(colorScheme.middle, "mono", { size: "sm" }), - path: { - ...text(colorScheme.middle, "mono", { size: "sm" }), - margin: { - left: 12, - }, - }, - }, - errorDiagnostic: diagnostic(colorScheme.middle, "negative"), - warningDiagnostic: diagnostic(colorScheme.middle, "warning"), - informationDiagnostic: diagnostic(colorScheme.middle, "accent"), - hintDiagnostic: diagnostic(colorScheme.middle, "warning"), - invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"), - invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"), - invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"), - invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"), - hoverPopover: hoverPopover(colorScheme), - linkDefinition: { - color: syntax.linkUri.color, - underline: syntax.linkUri.underline, - }, - jumpIcon: { - color: foreground(layer, "on"), - iconWidth: 20, - buttonWidth: 20, - cornerRadius: 6, - padding: { - top: 6, - bottom: 6, - left: 6, - right: 6, - }, - hover: { - background: background(layer, "on", "hovered"), - }, - }, - scrollbar: { - width: 12, - minHeightFactor: 1.0, - track: { - border: border(layer, "variant", { left: true }), - }, - thumb: { - background: withOpacity(background(layer, "inverted"), 0.3), - border: { - width: 1, - color: borderColor(layer, "variant"), - top: false, - right: true, - left: true, - bottom: false, - }, - }, - git: { - deleted: isLight - ? withOpacity(colorScheme.ramps.red(0.5).hex(), 0.8) - : withOpacity(colorScheme.ramps.red(0.4).hex(), 0.8), - modified: isLight - ? withOpacity(colorScheme.ramps.yellow(0.5).hex(), 0.8) - : withOpacity(colorScheme.ramps.yellow(0.4).hex(), 0.8), - inserted: isLight - ? withOpacity(colorScheme.ramps.green(0.5).hex(), 0.8) - : withOpacity(colorScheme.ramps.green(0.4).hex(), 0.8), - }, - selections: isLight - ? withOpacity(colorScheme.ramps.blue(0.5).hex(), 0.8) - : withOpacity(colorScheme.ramps.blue(0.4).hex(), 0.8) - }, - compositionMark: { - underline: { - thickness: 1.0, - color: borderColor(layer), - }, - }, - syntax, - } -} diff --git a/styles/src/styleTree/feedback.ts b/styles/src/styleTree/feedback.ts deleted file mode 100644 index 5eef4b4279..0000000000 --- a/styles/src/styleTree/feedback.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, text } from "./components" - -export default function feedback(colorScheme: ColorScheme) { - let layer = colorScheme.highest - - return { - submit_button: { - ...text(layer, "mono", "on"), - background: background(layer, "on"), - cornerRadius: 6, - border: border(layer, "on"), - margin: { - right: 4, - }, - padding: { - bottom: 2, - left: 10, - right: 10, - top: 2, - }, - clicked: { - ...text(layer, "mono", "on", "pressed"), - background: background(layer, "on", "pressed"), - border: border(layer, "on", "pressed"), - }, - hover: { - ...text(layer, "mono", "on", "hovered"), - background: background(layer, "on", "hovered"), - border: border(layer, "on", "hovered"), - }, - }, - button_margin: 8, - info_text_default: text(layer, "sans", "default", { size: "xs" }), - link_text_default: text(layer, "sans", "default", { - size: "xs", - underline: true, - }), - link_text_hover: text(layer, "sans", "hovered", { - size: "xs", - underline: true, - }), - } -} diff --git a/styles/src/styleTree/hoverPopover.ts b/styles/src/styleTree/hoverPopover.ts deleted file mode 100644 index f8988f1f3a..0000000000 --- a/styles/src/styleTree/hoverPopover.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, foreground, text } from "./components" - -export default function HoverPopover(colorScheme: ColorScheme) { - let layer = colorScheme.middle - let baseContainer = { - background: background(layer), - cornerRadius: 8, - padding: { - left: 8, - right: 8, - top: 4, - bottom: 4, - }, - shadow: colorScheme.popoverShadow, - border: border(layer), - margin: { - left: -8, - }, - } - - return { - container: baseContainer, - infoContainer: { - ...baseContainer, - background: background(layer, "accent"), - border: border(layer, "accent"), - }, - warningContainer: { - ...baseContainer, - background: background(layer, "warning"), - border: border(layer, "warning"), - }, - errorContainer: { - ...baseContainer, - background: background(layer, "negative"), - border: border(layer, "negative"), - }, - blockStyle: { - padding: { top: 4 }, - }, - prose: text(layer, "sans", { size: "sm" }), - diagnosticSourceHighlight: { color: foreground(layer, "accent") }, - highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better - } -} diff --git a/styles/src/styleTree/incomingCallNotification.ts b/styles/src/styleTree/incomingCallNotification.ts deleted file mode 100644 index c42558059c..0000000000 --- a/styles/src/styleTree/incomingCallNotification.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, text } from "./components" - -export default function incomingCallNotification( - colorScheme: ColorScheme -): Object { - let layer = colorScheme.middle - const avatarSize = 48 - return { - windowHeight: 74, - windowWidth: 380, - background: background(layer), - callerContainer: { - padding: 12, - }, - callerAvatar: { - height: avatarSize, - width: avatarSize, - cornerRadius: avatarSize / 2, - }, - callerMetadata: { - margin: { left: 10 }, - }, - callerUsername: { - ...text(layer, "sans", { size: "sm", weight: "bold" }), - margin: { top: -3 }, - }, - callerMessage: { - ...text(layer, "sans", "variant", { size: "xs" }), - margin: { top: -3 }, - }, - worktreeRoots: { - ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }), - margin: { top: -3 }, - }, - buttonWidth: 96, - acceptButton: { - background: background(layer, "accent"), - border: border(layer, { left: true, bottom: true }), - ...text(layer, "sans", "positive", { - size: "xs", - weight: "extra_bold", - }), - }, - declineButton: { - border: border(layer, { left: true }), - ...text(layer, "sans", "negative", { - size: "xs", - weight: "extra_bold", - }), - }, - } -} diff --git a/styles/src/styleTree/picker.ts b/styles/src/styleTree/picker.ts deleted file mode 100644 index d84bd6fc7a..0000000000 --- a/styles/src/styleTree/picker.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { withOpacity } from "../theme/color" -import { background, border, text } from "./components" - -export default function picker(colorScheme: ColorScheme): any { - let layer = colorScheme.lowest - const container = { - background: background(layer), - border: border(layer), - shadow: colorScheme.modalShadow, - cornerRadius: 12, - padding: { - bottom: 4, - }, - } - const inputEditor = { - placeholderText: text(layer, "sans", "on", "disabled"), - selection: colorScheme.players[0], - text: text(layer, "mono", "on"), - border: border(layer, { bottom: true }), - padding: { - bottom: 8, - left: 16, - right: 16, - top: 8, - }, - margin: { - bottom: 4, - }, - } - const emptyInputEditor: any = { ...inputEditor } - delete emptyInputEditor.border - delete emptyInputEditor.margin - - return { - ...container, - emptyContainer: { - ...container, - padding: {}, - }, - item: { - padding: { - bottom: 4, - left: 12, - right: 12, - top: 4, - }, - margin: { - top: 1, - left: 4, - right: 4, - }, - cornerRadius: 8, - text: text(layer, "sans", "variant"), - highlightText: text(layer, "sans", "accent", { weight: "bold" }), - active: { - background: withOpacity( - background(layer, "base", "active"), - 0.5 - ), - text: text(layer, "sans", "base", "active"), - highlightText: text(layer, "sans", "accent", { - weight: "bold", - }), - }, - hover: { - background: withOpacity(background(layer, "hovered"), 0.5), - }, - }, - inputEditor, - emptyInputEditor, - noMatches: { - text: text(layer, "sans", "variant"), - padding: { - bottom: 8, - left: 16, - right: 16, - top: 8, - }, - }, - } -} diff --git a/styles/src/styleTree/projectDiagnostics.ts b/styles/src/styleTree/projectDiagnostics.ts deleted file mode 100644 index cf0f07dd8c..0000000000 --- a/styles/src/styleTree/projectDiagnostics.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, text } from "./components" - -export default function projectDiagnostics(colorScheme: ColorScheme) { - let layer = colorScheme.highest - return { - background: background(layer), - tabIconSpacing: 4, - tabIconWidth: 13, - tabSummarySpacing: 10, - emptyMessage: text(layer, "sans", "variant", { size: "md" }), - } -} diff --git a/styles/src/styleTree/projectPanel.ts b/styles/src/styleTree/projectPanel.ts deleted file mode 100644 index a86ae010b6..0000000000 --- a/styles/src/styleTree/projectPanel.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { withOpacity } from "../theme/color" -import { background, border, foreground, text } from "./components" - -export default function projectPanel(colorScheme: ColorScheme) { - const { isLight } = colorScheme - - let layer = colorScheme.middle - - let baseEntry = { - height: 22, - iconColor: foreground(layer, "variant"), - iconSize: 7, - iconSpacing: 5, - } - - let status = { - git: { - modified: isLight - ? colorScheme.ramps.yellow(0.6).hex() - : colorScheme.ramps.yellow(0.5).hex(), - inserted: isLight - ? colorScheme.ramps.green(0.45).hex() - : colorScheme.ramps.green(0.5).hex(), - conflict: isLight - ? colorScheme.ramps.red(0.6).hex() - : colorScheme.ramps.red(0.5).hex(), - }, - } - - let entry = { - ...baseEntry, - text: text(layer, "mono", "variant", { size: "sm" }), - hover: { - background: background(layer, "variant", "hovered"), - }, - active: { - background: colorScheme.isLight - ? withOpacity(background(layer, "active"), 0.5) - : background(layer, "active"), - text: text(layer, "mono", "active", { size: "sm" }), - }, - activeHover: { - background: background(layer, "active"), - text: text(layer, "mono", "active", { size: "sm" }), - }, - status, - } - - return { - openProjectButton: { - background: background(layer), - border: border(layer, "active"), - cornerRadius: 4, - margin: { - top: 16, - left: 16, - right: 16, - }, - padding: { - top: 3, - bottom: 3, - left: 7, - right: 7, - }, - ...text(layer, "sans", "default", { size: "sm" }), - hover: { - ...text(layer, "sans", "default", { size: "sm" }), - background: background(layer, "hovered"), - border: border(layer, "active"), - }, - }, - background: background(layer), - padding: { left: 6, right: 6, top: 0, bottom: 6 }, - indentWidth: 12, - entry, - draggedEntry: { - ...baseEntry, - text: text(layer, "mono", "on", { size: "sm" }), - background: withOpacity(background(layer, "on"), 0.9), - border: border(layer), - status, - }, - ignoredEntry: { - ...entry, - iconColor: foreground(layer, "disabled"), - text: text(layer, "mono", "disabled"), - active: { - ...entry.active, - iconColor: foreground(layer, "variant"), - }, - }, - cutEntry: { - ...entry, - text: text(layer, "mono", "disabled"), - active: { - background: background(layer, "active"), - text: text(layer, "mono", "disabled", { size: "sm" }), - }, - }, - filenameEditor: { - background: background(layer, "on"), - text: text(layer, "mono", "on", { size: "sm" }), - selection: colorScheme.players[0], - }, - } -} diff --git a/styles/src/styleTree/projectSharedNotification.ts b/styles/src/styleTree/projectSharedNotification.ts deleted file mode 100644 index d05eb1b0c5..0000000000 --- a/styles/src/styleTree/projectSharedNotification.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, text } from "./components" - -export default function projectSharedNotification( - colorScheme: ColorScheme -): Object { - let layer = colorScheme.middle - - const avatarSize = 48 - return { - windowHeight: 74, - windowWidth: 380, - background: background(layer), - ownerContainer: { - padding: 12, - }, - ownerAvatar: { - height: avatarSize, - width: avatarSize, - cornerRadius: avatarSize / 2, - }, - ownerMetadata: { - margin: { left: 10 }, - }, - ownerUsername: { - ...text(layer, "sans", { size: "sm", weight: "bold" }), - margin: { top: -3 }, - }, - message: { - ...text(layer, "sans", "variant", { size: "xs" }), - margin: { top: -3 }, - }, - worktreeRoots: { - ...text(layer, "sans", "variant", { size: "xs", weight: "bold" }), - margin: { top: -3 }, - }, - buttonWidth: 96, - openButton: { - background: background(layer, "accent"), - border: border(layer, { left: true, bottom: true }), - ...text(layer, "sans", "accent", { - size: "xs", - weight: "extra_bold", - }), - }, - dismissButton: { - border: border(layer, { left: true }), - ...text(layer, "sans", "variant", { - size: "xs", - weight: "extra_bold", - }), - }, - } -} diff --git a/styles/src/styleTree/search.ts b/styles/src/styleTree/search.ts deleted file mode 100644 index d69c4bb2d9..0000000000 --- a/styles/src/styleTree/search.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { withOpacity } from "../theme/color" -import { background, border, foreground, text } from "./components" - -export default function search(colorScheme: ColorScheme) { - let layer = colorScheme.highest - - // Search input - const editor = { - background: background(layer), - cornerRadius: 8, - minWidth: 200, - maxWidth: 500, - placeholderText: text(layer, "mono", "disabled"), - selection: colorScheme.players[0], - text: text(layer, "mono", "default"), - border: border(layer), - margin: { - right: 12, - }, - padding: { - top: 3, - bottom: 3, - left: 12, - right: 8, - }, - } - - const includeExcludeEditor = { - ...editor, - minWidth: 100, - maxWidth: 250, - } - - return { - // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive - matchBackground: withOpacity(foreground(layer, "accent"), 0.4), - optionButton: { - ...text(layer, "mono", "on"), - background: background(layer, "on"), - cornerRadius: 6, - border: border(layer, "on"), - margin: { - right: 4, - }, - padding: { - bottom: 2, - left: 10, - right: 10, - top: 2, - }, - active: { - ...text(layer, "mono", "on", "inverted"), - background: background(layer, "on", "inverted"), - border: border(layer, "on", "inverted"), - }, - clicked: { - ...text(layer, "mono", "on", "pressed"), - background: background(layer, "on", "pressed"), - border: border(layer, "on", "pressed"), - }, - hover: { - ...text(layer, "mono", "on", "hovered"), - background: background(layer, "on", "hovered"), - border: border(layer, "on", "hovered"), - }, - }, - editor, - invalidEditor: { - ...editor, - border: border(layer, "negative"), - }, - includeExcludeEditor, - invalidIncludeExcludeEditor: { - ...includeExcludeEditor, - border: border(layer, "negative"), - }, - matchIndex: { - ...text(layer, "mono", "variant"), - padding: { - left: 6, - }, - }, - optionButtonGroup: { - padding: { - left: 12, - right: 12, - }, - }, - includeExcludeInputs: { - ...text(layer, "mono", "variant"), - padding: { - right: 6, - }, - }, - resultsStatus: { - ...text(layer, "mono", "on"), - size: 18, - }, - dismissButton: { - color: foreground(layer, "variant"), - iconWidth: 12, - buttonWidth: 14, - padding: { - left: 10, - right: 10, - }, - hover: { - color: foreground(layer, "hovered"), - }, - }, - } -} diff --git a/styles/src/styleTree/sharedScreen.ts b/styles/src/styleTree/sharedScreen.ts deleted file mode 100644 index 2563e718ff..0000000000 --- a/styles/src/styleTree/sharedScreen.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background } from "./components" - -export default function sharedScreen(colorScheme: ColorScheme) { - let layer = colorScheme.highest - return { - background: background(layer), - } -} diff --git a/styles/src/styleTree/simpleMessageNotification.ts b/styles/src/styleTree/simpleMessageNotification.ts deleted file mode 100644 index 8d88f05c53..0000000000 --- a/styles/src/styleTree/simpleMessageNotification.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, foreground, text } from "./components" - -const headerPadding = 8 - -export default function simpleMessageNotification( - colorScheme: ColorScheme -): Object { - let layer = colorScheme.middle - return { - message: { - ...text(layer, "sans", { size: "xs" }), - margin: { left: headerPadding, right: headerPadding }, - }, - actionMessage: { - ...text(layer, "sans", { size: "xs" }), - border: border(layer, "active"), - cornerRadius: 4, - padding: { - top: 3, - bottom: 3, - left: 7, - right: 7, - }, - - margin: { left: headerPadding, top: 6, bottom: 6 }, - hover: { - ...text(layer, "sans", "default", { size: "xs" }), - background: background(layer, "hovered"), - border: border(layer, "active"), - }, - }, - dismissButton: { - color: foreground(layer), - iconWidth: 8, - iconHeight: 8, - buttonWidth: 8, - buttonHeight: 8, - hover: { - color: foreground(layer, "hovered"), - }, - }, - } -} diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts deleted file mode 100644 index a8d926f40e..0000000000 --- a/styles/src/styleTree/statusBar.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, foreground, text } from "./components" - -export default function statusBar(colorScheme: ColorScheme) { - let layer = colorScheme.lowest - - const statusContainer = { - cornerRadius: 6, - padding: { top: 3, bottom: 3, left: 6, right: 6 }, - } - - const diagnosticStatusContainer = { - cornerRadius: 6, - padding: { top: 1, bottom: 1, left: 6, right: 6 }, - } - - return { - height: 30, - itemSpacing: 8, - padding: { - top: 1, - bottom: 1, - left: 6, - right: 6, - }, - border: border(layer, { top: true, overlay: true }), - cursorPosition: text(layer, "sans", "variant"), - activeLanguage: { - padding: { left: 6, right: 6 }, - ...text(layer, "sans", "variant"), - hover: { - ...text(layer, "sans", "on"), - }, - }, - autoUpdateProgressMessage: text(layer, "sans", "variant"), - autoUpdateDoneMessage: text(layer, "sans", "variant"), - lspStatus: { - ...diagnosticStatusContainer, - iconSpacing: 4, - iconWidth: 14, - height: 18, - message: text(layer, "sans"), - iconColor: foreground(layer), - hover: { - message: text(layer, "sans"), - iconColor: foreground(layer), - background: background(layer, "hovered"), - }, - }, - diagnosticMessage: { - ...text(layer, "sans"), - hover: text(layer, "sans", "hovered"), - }, - diagnosticSummary: { - height: 20, - iconWidth: 16, - iconSpacing: 2, - summarySpacing: 6, - text: text(layer, "sans", { size: "sm" }), - iconColorOk: foreground(layer, "variant"), - iconColorWarning: foreground(layer, "warning"), - iconColorError: foreground(layer, "negative"), - containerOk: { - cornerRadius: 6, - padding: { top: 3, bottom: 3, left: 7, right: 7 }, - }, - containerWarning: { - ...diagnosticStatusContainer, - background: background(layer, "warning"), - border: border(layer, "warning"), - }, - containerError: { - ...diagnosticStatusContainer, - background: background(layer, "negative"), - border: border(layer, "negative"), - }, - hover: { - iconColorOk: foreground(layer, "on"), - containerOk: { - cornerRadius: 6, - padding: { top: 3, bottom: 3, left: 7, right: 7 }, - background: background(layer, "on", "hovered"), - }, - containerWarning: { - ...diagnosticStatusContainer, - background: background(layer, "warning", "hovered"), - border: border(layer, "warning", "hovered"), - }, - containerError: { - ...diagnosticStatusContainer, - background: background(layer, "negative", "hovered"), - border: border(layer, "negative", "hovered"), - }, - }, - }, - panelButtons: { - groupLeft: {}, - groupBottom: {}, - groupRight: {}, - button: { - ...statusContainer, - iconSize: 16, - iconColor: foreground(layer, "variant"), - label: { - margin: { left: 6 }, - ...text(layer, "sans", { size: "sm" }), - }, - hover: { - iconColor: foreground(layer, "hovered"), - background: background(layer, "variant"), - }, - active: { - iconColor: foreground(layer, "active"), - background: background(layer, "active"), - }, - }, - badge: { - cornerRadius: 3, - padding: 2, - margin: { bottom: -1, right: -1 }, - border: border(layer), - background: background(layer, "accent"), - }, - }, - } -} diff --git a/styles/src/styleTree/tabBar.ts b/styles/src/styleTree/tabBar.ts deleted file mode 100644 index c5b397b34a..0000000000 --- a/styles/src/styleTree/tabBar.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { withOpacity } from "../theme/color" -import { text, border, background, foreground } from "./components" - -export default function tabBar(colorScheme: ColorScheme) { - const height = 32 - - let activeLayer = colorScheme.highest - let layer = colorScheme.middle - - const tab = { - height, - text: text(layer, "sans", "variant", { size: "sm" }), - background: background(layer), - border: border(layer, { - right: true, - bottom: true, - overlay: true, - }), - padding: { - left: 8, - right: 12, - }, - spacing: 8, - - // Tab type icons (e.g. Project Search) - typeIconWidth: 14, - - // Close icons - closeIconWidth: 8, - iconClose: foreground(layer, "variant"), - iconCloseActive: foreground(layer, "hovered"), - - // Indicators - iconConflict: foreground(layer, "warning"), - iconDirty: foreground(layer, "accent"), - - // When two tabs of the same name are open, a label appears next to them - description: { - margin: { left: 8 }, - ...text(layer, "sans", "disabled", { size: "2xs" }), - }, - } - - const activePaneActiveTab = { - ...tab, - background: background(activeLayer), - text: text(activeLayer, "sans", "active", { size: "sm" }), - border: { - ...tab.border, - bottom: false, - }, - } - - const inactivePaneInactiveTab = { - ...tab, - background: background(layer), - text: text(layer, "sans", "variant", { size: "sm" }), - } - - const inactivePaneActiveTab = { - ...tab, - background: background(activeLayer), - text: text(layer, "sans", "variant", { size: "sm" }), - border: { - ...tab.border, - bottom: false, - }, - } - - const draggedTab = { - ...activePaneActiveTab, - background: withOpacity(tab.background, 0.9), - border: undefined as any, - shadow: colorScheme.popoverShadow, - } - - return { - height, - background: background(layer), - activePane: { - activeTab: activePaneActiveTab, - inactiveTab: tab, - }, - inactivePane: { - activeTab: inactivePaneActiveTab, - inactiveTab: inactivePaneInactiveTab, - }, - draggedTab, - paneButton: { - color: foreground(layer, "variant"), - iconWidth: 12, - buttonWidth: activePaneActiveTab.height, - hover: { - color: foreground(layer, "hovered"), - }, - active: { - color: foreground(layer, "accent"), - }, - }, - paneButtonContainer: { - background: tab.background, - border: { - ...tab.border, - right: false, - }, - }, - } -} diff --git a/styles/src/styleTree/terminal.ts b/styles/src/styleTree/terminal.ts deleted file mode 100644 index 8a7eb7a549..0000000000 --- a/styles/src/styleTree/terminal.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" - -export default function terminal(colorScheme: ColorScheme) { - /** - * Colors are controlled per-cell in the terminal grid. - * Cells can be set to any of these more 'theme-capable' colors - * or can be set directly with RGB values. - * Here are the common interpretations of these names: - * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors - */ - return { - black: colorScheme.ramps.neutral(0).hex(), - red: colorScheme.ramps.red(0.5).hex(), - green: colorScheme.ramps.green(0.5).hex(), - yellow: colorScheme.ramps.yellow(0.5).hex(), - blue: colorScheme.ramps.blue(0.5).hex(), - magenta: colorScheme.ramps.magenta(0.5).hex(), - cyan: colorScheme.ramps.cyan(0.5).hex(), - white: colorScheme.ramps.neutral(1).hex(), - brightBlack: colorScheme.ramps.neutral(0.4).hex(), - brightRed: colorScheme.ramps.red(0.25).hex(), - brightGreen: colorScheme.ramps.green(0.25).hex(), - brightYellow: colorScheme.ramps.yellow(0.25).hex(), - brightBlue: colorScheme.ramps.blue(0.25).hex(), - brightMagenta: colorScheme.ramps.magenta(0.25).hex(), - brightCyan: colorScheme.ramps.cyan(0.25).hex(), - brightWhite: colorScheme.ramps.neutral(1).hex(), - /** - * Default color for characters - */ - foreground: colorScheme.ramps.neutral(1).hex(), - /** - * Default color for the rectangle background of a cell - */ - background: colorScheme.ramps.neutral(0).hex(), - modalBackground: colorScheme.ramps.neutral(0.1).hex(), - /** - * Default color for the cursor - */ - cursor: colorScheme.players[0].cursor, - dimBlack: colorScheme.ramps.neutral(1).hex(), - dimRed: colorScheme.ramps.red(0.75).hex(), - dimGreen: colorScheme.ramps.green(0.75).hex(), - dimYellow: colorScheme.ramps.yellow(0.75).hex(), - dimBlue: colorScheme.ramps.blue(0.75).hex(), - dimMagenta: colorScheme.ramps.magenta(0.75).hex(), - dimCyan: colorScheme.ramps.cyan(0.75).hex(), - dimWhite: colorScheme.ramps.neutral(0.6).hex(), - brightForeground: colorScheme.ramps.neutral(1).hex(), - dimForeground: colorScheme.ramps.neutral(0).hex(), - } -} diff --git a/styles/src/styleTree/toolbarDropdownMenu.ts b/styles/src/styleTree/toolbarDropdownMenu.ts deleted file mode 100644 index 92616eb022..0000000000 --- a/styles/src/styleTree/toolbarDropdownMenu.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, text } from "./components" - -export default function dropdownMenu(colorScheme: ColorScheme) { - let layer = colorScheme.middle - - return { - rowHeight: 30, - background: background(layer), - border: border(layer), - shadow: colorScheme.popoverShadow, - header: { - ...text(layer, "sans", { size: "sm" }), - secondaryText: text(layer, "sans", { size: "sm", color: "#aaaaaa" }), - secondaryTextSpacing: 10, - padding: { left: 8, right: 8, top: 2, bottom: 2 }, - cornerRadius: 6, - background: background(layer, "on"), - border: border(layer, "on", { overlay: true }), - hover: { - background: background(layer, "hovered"), - ...text(layer, "sans", "hovered", { size: "sm" }), - } - }, - sectionHeader: { - ...text(layer, "sans", { size: "sm" }), - padding: { left: 8, right: 8, top: 8, bottom: 8 }, - }, - item: { - ...text(layer, "sans", { size: "sm" }), - secondaryTextSpacing: 10, - secondaryText: text(layer, "sans", { size: "sm" }), - padding: { left: 18, right: 18, top: 2, bottom: 2 }, - hover: { - background: background(layer, "hovered"), - ...text(layer, "sans", "hovered", { size: "sm" }), - }, - active: { - background: background(layer, "active"), - }, - activeHover: { - background: background(layer, "active"), - }, - }, - } -} diff --git a/styles/src/styleTree/tooltip.ts b/styles/src/styleTree/tooltip.ts deleted file mode 100644 index 1666ce5658..0000000000 --- a/styles/src/styleTree/tooltip.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { background, border, text } from "./components" - -export default function tooltip(colorScheme: ColorScheme) { - let layer = colorScheme.middle - return { - background: background(layer), - border: border(layer), - padding: { top: 4, bottom: 4, left: 8, right: 8 }, - margin: { top: 6, left: 6 }, - shadow: colorScheme.popoverShadow, - cornerRadius: 6, - text: text(layer, "sans", { size: "xs" }), - keystroke: { - background: background(layer, "on"), - cornerRadius: 4, - margin: { left: 6 }, - padding: { left: 4, right: 4 }, - ...text(layer, "mono", "on", { size: "xs", weight: "bold" }), - }, - maxTextWidth: 200, - } -} diff --git a/styles/src/styleTree/updateNotification.ts b/styles/src/styleTree/updateNotification.ts deleted file mode 100644 index 281012e62f..0000000000 --- a/styles/src/styleTree/updateNotification.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { foreground, text } from "./components" - -const headerPadding = 8 - -export default function updateNotification(colorScheme: ColorScheme): Object { - let layer = colorScheme.middle - return { - message: { - ...text(layer, "sans", { size: "xs" }), - margin: { left: headerPadding, right: headerPadding }, - }, - actionMessage: { - ...text(layer, "sans", { size: "xs" }), - margin: { left: headerPadding, top: 6, bottom: 6 }, - hover: { - color: foreground(layer, "hovered"), - }, - }, - dismissButton: { - color: foreground(layer), - iconWidth: 8, - iconHeight: 8, - buttonWidth: 8, - buttonHeight: 8, - hover: { - color: foreground(layer, "hovered"), - }, - }, - } -} diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts deleted file mode 100644 index 10e6e02b95..0000000000 --- a/styles/src/styleTree/welcome.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { withOpacity } from "../theme/color" -import { - border, - background, - foreground, - text, - TextProperties, - svg, -} from "./components" - -export default function welcome(colorScheme: ColorScheme) { - let layer = colorScheme.highest - - let checkboxBase = { - cornerRadius: 4, - padding: { - left: 3, - right: 3, - top: 3, - bottom: 3, - }, - // shadow: colorScheme.popoverShadow, - border: border(layer), - margin: { - right: 8, - top: 5, - bottom: 5, - }, - } - - let interactive_text_size: TextProperties = { size: "sm" } - - return { - pageWidth: 320, - logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64), - logoSubheading: { - ...text(layer, "sans", "variant", { size: "md" }), - margin: { - top: 10, - bottom: 7, - }, - }, - buttonGroup: { - margin: { - top: 8, - bottom: 16, - }, - }, - headingGroup: { - margin: { - top: 8, - bottom: 12, - }, - }, - checkboxGroup: { - border: border(layer, "variant"), - background: withOpacity(background(layer, "hovered"), 0.25), - cornerRadius: 4, - padding: { - left: 12, - top: 2, - bottom: 2, - }, - }, - button: { - background: background(layer), - border: border(layer, "active"), - cornerRadius: 4, - margin: { - top: 4, - bottom: 4, - }, - padding: { - top: 3, - bottom: 3, - left: 7, - right: 7, - }, - ...text(layer, "sans", "default", interactive_text_size), - hover: { - ...text(layer, "sans", "default", interactive_text_size), - background: background(layer, "hovered"), - border: border(layer, "active"), - }, - }, - usageNote: { - ...text(layer, "sans", "variant", { size: "2xs" }), - padding: { - top: -4, - }, - }, - checkboxContainer: { - margin: { - top: 4, - }, - padding: { - bottom: 8, - }, - }, - checkbox: { - label: { - ...text(layer, "sans", interactive_text_size), - // Also supports margin, container, border, etc. - }, - icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12), - default: { - ...checkboxBase, - background: background(layer, "default"), - border: border(layer, "active"), - }, - checked: { - ...checkboxBase, - background: background(layer, "hovered"), - border: border(layer, "active"), - }, - hovered: { - ...checkboxBase, - background: background(layer, "hovered"), - border: border(layer, "active"), - }, - hoveredAndChecked: { - ...checkboxBase, - background: background(layer, "hovered"), - border: border(layer, "active"), - }, - }, - } -} diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts deleted file mode 100644 index ae8de178f8..0000000000 --- a/styles/src/styleTree/workspace.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { ColorScheme } from "../theme/colorScheme" -import { withOpacity } from "../theme/color" -import { - background, - border, - borderColor, - foreground, - svg, - text, -} from "./components" -import statusBar from "./statusBar" -import tabBar from "./tabBar" - -export default function workspace(colorScheme: ColorScheme) { - const layer = colorScheme.lowest - const isLight = colorScheme.isLight - const itemSpacing = 8 - const titlebarButton = { - cornerRadius: 6, - padding: { - top: 1, - bottom: 1, - left: 8, - right: 8, - }, - ...text(layer, "sans", "variant", { size: "xs" }), - background: background(layer, "variant"), - border: border(layer), - hover: { - ...text(layer, "sans", "variant", "hovered", { size: "xs" }), - background: background(layer, "variant", "hovered"), - border: border(layer, "variant", "hovered"), - }, - clicked: { - ...text(layer, "sans", "variant", "pressed", { size: "xs" }), - background: background(layer, "variant", "pressed"), - border: border(layer, "variant", "pressed"), - }, - active: { - ...text(layer, "sans", "variant", "active", { size: "xs" }), - background: background(layer, "variant", "active"), - border: border(layer, "variant", "active"), - }, - } - const avatarWidth = 18 - const avatarOuterWidth = avatarWidth + 4 - const followerAvatarWidth = 14 - const followerAvatarOuterWidth = followerAvatarWidth + 4 - - return { - background: background(colorScheme.lowest), - blankPane: { - logoContainer: { - width: 256, - height: 256, - }, - logo: svg( - withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8), - "icons/logo_96.svg", - 256, - 256 - ), - - logoShadow: svg( - withOpacity( - colorScheme.isLight - ? "#FFFFFF" - : colorScheme.lowest.base.default.background, - colorScheme.isLight ? 1 : 0.6 - ), - "icons/logo_96.svg", - 256, - 256 - ), - keyboardHints: { - margin: { - top: 96, - }, - cornerRadius: 4, - }, - keyboardHint: { - ...text(layer, "sans", "variant", { size: "sm" }), - padding: { - top: 3, - left: 8, - right: 8, - bottom: 3, - }, - cornerRadius: 8, - hover: { - ...text(layer, "sans", "active", { size: "sm" }), - }, - }, - keyboardHintWidth: 320, - }, - joiningProjectAvatar: { - cornerRadius: 40, - width: 80, - }, - joiningProjectMessage: { - padding: 12, - ...text(layer, "sans", { size: "lg" }), - }, - externalLocationMessage: { - background: background(colorScheme.middle, "accent"), - border: border(colorScheme.middle, "accent"), - cornerRadius: 6, - padding: 12, - margin: { bottom: 8, right: 8 }, - ...text(colorScheme.middle, "sans", "accent", { size: "xs" }), - }, - leaderBorderOpacity: 0.7, - leaderBorderWidth: 2.0, - tabBar: tabBar(colorScheme), - modal: { - margin: { - bottom: 52, - top: 52, - }, - cursor: "Arrow", - }, - zoomedBackground: { - cursor: "Arrow", - background: isLight - ? withOpacity(background(colorScheme.lowest), 0.8) - : withOpacity(background(colorScheme.highest), 0.6), - }, - zoomedPaneForeground: { - margin: 16, - shadow: colorScheme.modalShadow, - border: border(colorScheme.lowest, { overlay: true }), - }, - zoomedPanelForeground: { - margin: 16, - border: border(colorScheme.lowest, { overlay: true }), - }, - dock: { - left: { - border: border(layer, { right: true }), - }, - bottom: { - border: border(layer, { top: true }), - }, - right: { - border: border(layer, { left: true }), - }, - }, - paneDivider: { - color: borderColor(layer), - width: 1, - }, - statusBar: statusBar(colorScheme), - titlebar: { - itemSpacing, - facePileSpacing: 2, - height: 33, // 32px + 1px border. It's important the content area of the titlebar is evenly sized to vertically center avatar images. - background: background(layer), - border: border(layer, { bottom: true }), - padding: { - left: 80, - right: itemSpacing, - }, - - // Project - title: text(layer, "sans", "variant"), - highlight_color: text(layer, "sans", "active").color, - - // Collaborators - leaderAvatar: { - width: avatarWidth, - outerWidth: avatarOuterWidth, - cornerRadius: avatarWidth / 2, - outerCornerRadius: avatarOuterWidth / 2, - }, - followerAvatar: { - width: followerAvatarWidth, - outerWidth: followerAvatarOuterWidth, - cornerRadius: followerAvatarWidth / 2, - outerCornerRadius: followerAvatarOuterWidth / 2, - }, - inactiveAvatarGrayscale: true, - followerAvatarOverlap: 8, - leaderSelection: { - margin: { - top: 4, - bottom: 4, - }, - padding: { - left: 2, - right: 2, - top: 2, - bottom: 2, - }, - cornerRadius: 6, - }, - avatarRibbon: { - height: 3, - width: 12, - // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded. - }, - - // Sign in buttom - // FlatButton, Variant - signInPrompt: { - margin: { - left: itemSpacing, - }, - ...titlebarButton, - }, - - // Offline Indicator - offlineIcon: { - color: foreground(layer, "variant"), - width: 16, - margin: { - left: itemSpacing, - }, - padding: { - right: 4, - }, - }, - - // Notice that the collaboration server is out of date - outdatedWarning: { - ...text(layer, "sans", "warning", { size: "xs" }), - background: withOpacity(background(layer, "warning"), 0.3), - border: border(layer, "warning"), - margin: { - left: itemSpacing, - }, - padding: { - left: 8, - right: 8, - }, - cornerRadius: 6, - }, - callControl: { - cornerRadius: 6, - color: foreground(layer, "variant"), - iconWidth: 12, - buttonWidth: 20, - hover: { - background: background(layer, "variant", "hovered"), - color: foreground(layer, "variant", "hovered"), - }, - }, - toggleContactsButton: { - margin: { left: itemSpacing }, - cornerRadius: 6, - color: foreground(layer, "variant"), - iconWidth: 14, - buttonWidth: 20, - active: { - background: background(layer, "variant", "active"), - color: foreground(layer, "variant", "active"), - }, - clicked: { - background: background(layer, "variant", "pressed"), - color: foreground(layer, "variant", "pressed"), - }, - hover: { - background: background(layer, "variant", "hovered"), - color: foreground(layer, "variant", "hovered"), - }, - }, - userMenuButton: { - buttonWidth: 20, - iconWidth: 12, - ...titlebarButton, - }, - toggleContactsBadge: { - cornerRadius: 3, - padding: 2, - margin: { top: 3, left: 3 }, - border: border(layer), - background: foreground(layer, "accent"), - }, - shareButton: { - ...titlebarButton, - }, - }, - - toolbar: { - height: 34, - background: background(colorScheme.highest), - border: border(colorScheme.highest, { bottom: true }), - itemSpacing: 8, - navButton: { - color: foreground(colorScheme.highest, "on"), - iconWidth: 12, - buttonWidth: 24, - cornerRadius: 6, - hover: { - color: foreground(colorScheme.highest, "on", "hovered"), - background: background( - colorScheme.highest, - "on", - "hovered" - ), - }, - disabled: { - color: foreground(colorScheme.highest, "on", "disabled"), - }, - }, - padding: { left: 8, right: 8, top: 4, bottom: 4 }, - }, - breadcrumbHeight: 24, - breadcrumbs: { - ...text(colorScheme.highest, "sans", "variant"), - cornerRadius: 6, - padding: { - left: 6, - right: 6, - }, - hover: { - color: foreground(colorScheme.highest, "on", "hovered"), - background: background(colorScheme.highest, "on", "hovered"), - }, - }, - disconnectedOverlay: { - ...text(layer, "sans"), - background: withOpacity(background(layer), 0.8), - }, - notification: { - margin: { top: 10 }, - background: background(colorScheme.middle), - cornerRadius: 6, - padding: 12, - border: border(colorScheme.middle), - shadow: colorScheme.popoverShadow, - }, - notifications: { - width: 400, - margin: { right: 10, bottom: 10 }, - }, - dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5), - } -} diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts new file mode 100644 index 0000000000..ccfdd60a98 --- /dev/null +++ b/styles/src/style_tree/app.ts @@ -0,0 +1,62 @@ +import contact_finder from "./contact_finder" +import contacts_popover from "./contacts_popover" +import command_palette from "./command_palette" +import project_panel from "./project_panel" +import search from "./search" +import picker from "./picker" +import workspace from "./workspace" +import context_menu from "./context_menu" +import shared_screen from "./shared_screen" +import project_diagnostics from "./project_diagnostics" +import contact_notification from "./contact_notification" +import update_notification from "./update_notification" +import simple_message_notification from "./simple_message_notification" +import project_shared_notification from "./project_shared_notification" +import tooltip from "./tooltip" +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 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() + + return { + meta: { + name: theme.name, + is_light: theme.is_light, + }, + 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() + } +} diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts new file mode 100644 index 0000000000..6df02a0e33 --- /dev/null +++ b/styles/src/style_tree/assistant.ts @@ -0,0 +1,281 @@ +import { text, border, background, foreground } from "./components" +import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function assistant(): any { + const theme = useTheme() + + return { + container: { + background: background(theme.highest), + padding: { left: 12 }, + }, + message_header: { + margin: { bottom: 6, top: 6 }, + background: background(theme.highest), + }, + hamburger_button: interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/hamburger_15.svg", + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + padding: { left: 12, right: 8.5 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }), + split_button: interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/split_message_15.svg", + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + padding: { left: 8.5, right: 8.5 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }), + quote_button: interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/quote_15.svg", + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + padding: { left: 8.5, right: 8.5 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }), + assist_button: interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/assist_15.svg", + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + padding: { left: 8.5, right: 8.5 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }), + zoom_in_button: interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/maximize_8.svg", + dimensions: { + width: 12, + height: 12, + }, + }, + container: { + padding: { left: 10, right: 10 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }), + zoom_out_button: interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/minimize_8.svg", + dimensions: { + width: 12, + height: 12, + }, + }, + container: { + padding: { left: 10, right: 10 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }), + plus_button: interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: "icons/plus_12.svg", + dimensions: { + width: 12, + height: 12, + }, + }, + container: { + padding: { left: 10, right: 10 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }), + title: { + ...text(theme.highest, "sans", "default", { size: "sm" }), + }, + saved_conversation: { + container: interactive({ + base: { + background: background(theme.highest, "on"), + padding: { top: 4, bottom: 4 }, + }, + state: { + hovered: { + background: background(theme.highest, "on", "hovered"), + }, + }, + }), + saved_at: { + margin: { left: 8 }, + ...text(theme.highest, "sans", "default", { size: "xs" }), + }, + title: { + margin: { left: 16 }, + ...text(theme.highest, "sans", "default", { + size: "sm", + weight: "bold", + }), + }, + }, + user_sender: { + default: { + ...text(theme.highest, "sans", "default", { + size: "sm", + weight: "bold", + }), + }, + }, + assistant_sender: { + default: { + ...text(theme.highest, "sans", "accent", { + size: "sm", + weight: "bold", + }), + }, + }, + system_sender: { + default: { + ...text(theme.highest, "sans", "variant", { + size: "sm", + weight: "bold", + }), + }, + }, + sent_at: { + margin: { top: 2, left: 8 }, + ...text(theme.highest, "sans", "default", { size: "2xs" }), + }, + model: interactive({ + base: { + background: background(theme.highest, "on"), + margin: { left: 12, right: 12, top: 12 }, + padding: 4, + corner_radius: 4, + ...text(theme.highest, "sans", "default", { size: "xs" }), + }, + state: { + hovered: { + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", { overlay: true }), + }, + }, + }), + remaining_tokens: { + background: background(theme.highest, "on"), + margin: { top: 12, right: 24 }, + padding: 4, + corner_radius: 4, + ...text(theme.highest, "sans", "positive", { size: "xs" }), + }, + no_remaining_tokens: { + background: background(theme.highest, "on"), + margin: { top: 12, right: 24 }, + padding: 4, + corner_radius: 4, + ...text(theme.highest, "sans", "negative", { size: "xs" }), + }, + error_icon: { + margin: { left: 8 }, + color: foreground(theme.highest, "negative"), + width: 12, + }, + api_key_editor: { + background: background(theme.highest, "on"), + corner_radius: 6, + text: text(theme.highest, "mono", "on"), + placeholder_text: text(theme.highest, "mono", "on", "disabled", { + size: "xs", + }), + selection: theme.players[0], + border: border(theme.highest, "on"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + }, + api_key_prompt: { + padding: 10, + ...text(theme.highest, "sans", "default", { size: "xs" }), + }, + } +} diff --git a/styles/src/style_tree/command_palette.ts b/styles/src/style_tree/command_palette.ts new file mode 100644 index 0000000000..2f7404c8d4 --- /dev/null +++ b/styles/src/style_tree/command_palette.ts @@ -0,0 +1,46 @@ +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() + + const key = toggleable({ + base: { + text: text(theme.highest, "mono", "variant", "default", { + size: "xs", + }), + corner_radius: 2, + background: background(theme.highest, "on"), + padding: { + top: 1, + bottom: 1, + left: 6, + right: 6, + }, + margin: { + top: 1, + bottom: 1, + left: 2, + }, + }, + state: { + active: { + text: text(theme.highest, "mono", "on", "default", { + size: "xs", + }), + background: with_opacity(background(theme.highest, "on"), 0.2), + }, + }, + }) + + return { + keystroke_spacing: 8, + // TODO: This should be a Toggle on the rust side so we don't have to do this + key: { + inactive: { ...key.inactive }, + active: key.active, + }, + } +} diff --git a/styles/src/styleTree/components.ts b/styles/src/style_tree/components.ts similarity index 66% rename from styles/src/styleTree/components.ts rename to styles/src/style_tree/components.ts index a575dad527..43a5fa9d28 100644 --- a/styles/src/styleTree/components.ts +++ b/styles/src/style_tree/components.ts @@ -1,7 +1,7 @@ -import { fontFamilies, fontSizes, FontWeight } from "../common" -import { Layer, Styles, StyleSets, Style } from "../theme/colorScheme" +import { font_families, font_sizes, FontWeight } from "../common" +import { Layer, Styles, StyleSets, Style } from "../theme/create_theme" -function isStyleSet(key: any): key is StyleSets { +function is_style_set(key: any): key is StyleSets { return [ "base", "variant", @@ -13,7 +13,7 @@ function isStyleSet(key: any): key is StyleSets { ].includes(key) } -function isStyle(key: any): key is Styles { +function is_style(key: any): key is Styles { return [ "default", "active", @@ -23,70 +23,70 @@ function isStyle(key: any): key is Styles { "inverted", ].includes(key) } -function getStyle( +function get_style( layer: Layer, - possibleStyleSetOrStyle?: any, - possibleStyle?: any + possible_style_set_or_style?: any, + possible_style?: any ): Style { - let styleSet: StyleSets = "base" + let style_set: StyleSets = "base" let style: Styles = "default" - if (isStyleSet(possibleStyleSetOrStyle)) { - styleSet = possibleStyleSetOrStyle - } else if (isStyle(possibleStyleSetOrStyle)) { - style = possibleStyleSetOrStyle + if (is_style_set(possible_style_set_or_style)) { + style_set = possible_style_set_or_style + } else if (is_style(possible_style_set_or_style)) { + style = possible_style_set_or_style } - if (isStyle(possibleStyle)) { - style = possibleStyle + if (is_style(possible_style)) { + style = possible_style } - return layer[styleSet][style] + return layer[style_set][style] } export function background(layer: Layer, style?: Styles): string export function background( layer: Layer, - styleSet?: StyleSets, + style_set?: StyleSets, style?: Styles ): string export function background( layer: Layer, - styleSetOrStyles?: StyleSets | Styles, + style_set_or_styles?: StyleSets | Styles, style?: Styles ): string { - return getStyle(layer, styleSetOrStyles, style).background + return get_style(layer, style_set_or_styles, style).background } -export function borderColor(layer: Layer, style?: Styles): string -export function borderColor( +export function border_color(layer: Layer, style?: Styles): string +export function border_color( layer: Layer, - styleSet?: StyleSets, + style_set?: StyleSets, style?: Styles ): string -export function borderColor( +export function border_color( layer: Layer, - styleSetOrStyles?: StyleSets | Styles, + style_set_or_styles?: StyleSets | Styles, style?: Styles ): string { - return getStyle(layer, styleSetOrStyles, style).border + return get_style(layer, style_set_or_styles, style).border } export function foreground(layer: Layer, style?: Styles): string export function foreground( layer: Layer, - styleSet?: StyleSets, + style_set?: StyleSets, style?: Styles ): string export function foreground( layer: Layer, - styleSetOrStyles?: StyleSets | Styles, + style_set_or_styles?: StyleSets | Styles, style?: Styles ): string { - return getStyle(layer, styleSetOrStyles, style).foreground + return get_style(layer, style_set_or_styles, style).foreground } -interface Text { - family: keyof typeof fontFamilies +export interface TextStyle extends Object { + family: keyof typeof font_families color: string size: number weight?: FontWeight @@ -94,7 +94,7 @@ interface Text { } export interface TextProperties { - size?: keyof typeof fontSizes + size?: keyof typeof font_sizes weight?: FontWeight underline?: boolean color?: string @@ -174,49 +174,53 @@ interface FontFeatures { export function text( layer: Layer, - fontFamily: keyof typeof fontFamilies, - styleSet: StyleSets, + font_family: keyof typeof font_families, + style_set: StyleSets, style: Styles, properties?: TextProperties -): Text +): TextStyle export function text( layer: Layer, - fontFamily: keyof typeof fontFamilies, - styleSet: StyleSets, + font_family: keyof typeof font_families, + style_set: StyleSets, properties?: TextProperties -): Text +): TextStyle export function text( layer: Layer, - fontFamily: keyof typeof fontFamilies, + font_family: keyof typeof font_families, style: Styles, properties?: TextProperties -): Text +): TextStyle export function text( layer: Layer, - fontFamily: keyof typeof fontFamilies, + font_family: keyof typeof font_families, properties?: TextProperties -): Text +): TextStyle export function text( layer: Layer, - fontFamily: keyof typeof fontFamilies, - styleSetStyleOrProperties?: StyleSets | Styles | TextProperties, - styleOrProperties?: Styles | TextProperties, + font_family: keyof typeof font_families, + style_set_style_or_properties?: StyleSets | Styles | TextProperties, + style_or_properties?: Styles | TextProperties, properties?: TextProperties ) { - let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties) + const style = get_style( + layer, + style_set_style_or_properties, + style_or_properties + ) - if (typeof styleSetStyleOrProperties === "object") { - properties = styleSetStyleOrProperties + if (typeof style_set_style_or_properties === "object") { + properties = style_set_style_or_properties } - if (typeof styleOrProperties === "object") { - properties = styleOrProperties + if (typeof style_or_properties === "object") { + properties = style_or_properties } - let size = fontSizes[properties?.size || "sm"] - let color = properties?.color || style.foreground + const size = font_sizes[properties?.size || "sm"] + const color = properties?.color || style.foreground return { - family: fontFamilies[fontFamily], + family: font_families[font_family], ...properties, color, size, @@ -244,13 +248,13 @@ export interface BorderProperties { export function border( layer: Layer, - styleSet: StyleSets, + style_set: StyleSets, style: Styles, properties?: BorderProperties ): Border export function border( layer: Layer, - styleSet: StyleSets, + style_set: StyleSets, properties?: BorderProperties ): Border export function border( @@ -261,17 +265,17 @@ export function border( export function border(layer: Layer, properties?: BorderProperties): Border export function border( layer: Layer, - styleSetStyleOrProperties?: StyleSets | Styles | BorderProperties, - styleOrProperties?: Styles | BorderProperties, + style_set_or_properties?: StyleSets | Styles | BorderProperties, + style_or_properties?: Styles | BorderProperties, properties?: BorderProperties ): Border { - let style = getStyle(layer, styleSetStyleOrProperties, styleOrProperties) + const style = get_style(layer, style_set_or_properties, style_or_properties) - if (typeof styleSetStyleOrProperties === "object") { - properties = styleSetStyleOrProperties + if (typeof style_set_or_properties === "object") { + properties = style_set_or_properties } - if (typeof styleOrProperties === "object") { - properties = styleOrProperties + if (typeof style_or_properties === "object") { + properties = style_or_properties } return { @@ -283,9 +287,9 @@ export function border( export function svg( color: string, - asset: String, - width: Number, - height: Number + asset: string, + width: number, + height: number ) { return { color, diff --git a/styles/src/style_tree/contact_finder.ts b/styles/src/style_tree/contact_finder.ts new file mode 100644 index 0000000000..aa88a9f26a --- /dev/null +++ b/styles/src/style_tree/contact_finder.ts @@ -0,0 +1,74 @@ +import picker from "./picker" +import { background, border, foreground, text } from "./components" +import { useTheme } from "../theme" + +export default function contact_finder(): any { + const theme = useTheme() + + const side_margin = 6 + const contact_button = { + background: background(theme.middle, "variant"), + color: foreground(theme.middle, "variant"), + icon_width: 8, + button_width: 16, + corner_radius: 8, + } + + const picker_style = picker() + const picker_input = { + background: background(theme.middle, "on"), + corner_radius: 6, + text: text(theme.middle, "mono"), + placeholder_text: text(theme.middle, "mono", "on", "disabled", { + size: "xs", + }), + selection: theme.players[0], + border: border(theme.middle), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: side_margin, + right: side_margin, + }, + } + + return { + picker: { + empty_container: {}, + item: { + ...picker_style.item, + margin: { left: side_margin, right: side_margin }, + }, + 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: { + corner_radius: 10, + width: 18, + }, + contact_username: { + padding: { + left: 8, + }, + }, + contact_button: { + ...contact_button, + hover: { + background: background(theme.middle, "variant", "hovered"), + }, + }, + disabled_contact_button: { + ...contact_button, + background: background(theme.middle, "disabled"), + color: foreground(theme.middle, "disabled"), + }, + } +} diff --git a/styles/src/style_tree/contact_list.ts b/styles/src/style_tree/contact_list.ts new file mode 100644 index 0000000000..1955231f59 --- /dev/null +++ b/styles/src/style_tree/contact_list.ts @@ -0,0 +1,247 @@ +import { + background, + border, + border_color, + foreground, + text, +} from "./components" +import { interactive, toggleable } from "../element" +import { useTheme } from "../theme" +export default function contacts_panel(): any { + const theme = useTheme() + + const name_margin = 8 + const side_padding = 12 + + const layer = theme.middle + + const contact_button = { + background: background(layer, "on"), + color: foreground(layer, "on"), + icon_width: 8, + button_width: 16, + corner_radius: 8, + } + const project_row = { + guest_avatar_spacing: 4, + height: 24, + guest_avatar: { + corner_radius: 8, + width: 14, + }, + name: { + ...text(layer, "mono", { size: "sm" }), + margin: { + left: name_margin, + right: 6, + }, + }, + guests: { + margin: { + left: name_margin, + right: name_margin, + }, + }, + padding: { + left: side_padding, + right: side_padding, + }, + } + + return { + background: background(layer), + padding: { top: 12 }, + user_query_editor: { + background: background(layer, "on"), + corner_radius: 6, + text: text(layer, "mono", "on"), + placeholder_text: text(layer, "mono", "on", "disabled", { + size: "xs", + }), + selection: theme.players[0], + border: border(layer, "on"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: 6, + }, + }, + user_query_editor_height: 33, + add_contact_button: { + margin: { left: 6, right: 12 }, + color: foreground(layer, "on"), + button_width: 28, + icon_width: 16, + }, + row_height: 28, + section_icon_size: 8, + header_row: toggleable({ + base: interactive({ + base: { + ...text(layer, "mono", { size: "sm" }), + margin: { top: 14 }, + padding: { + left: side_padding, + right: side_padding, + }, + background: background(layer, "default"), // posiewic: breaking change + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, // hack, we want headerRow to be interactive for whatever reason. It probably shouldn't be interactive in the first place. + }), + state: { + active: { + default: { + ...text(layer, "mono", "active", { size: "sm" }), + background: background(layer, "active"), + }, + hovered: { + background: background(layer, "hovered"), + }, + clicked: { + background: background(layer, "pressed"), + }, + }, + }, + }), + leave_call: interactive({ + base: { + background: background(layer), + border: border(layer), + corner_radius: 6, + margin: { + top: 1, + }, + padding: { + top: 1, + bottom: 1, + left: 7, + right: 7, + }, + ...text(layer, "sans", "variant", { size: "xs" }), + }, + state: { + hovered: { + ...text(layer, "sans", "hovered", { size: "xs" }), + background: background(layer, "hovered"), + border: border(layer, "hovered"), + }, + }, + }), + contact_row: { + inactive: { + default: { + padding: { + left: side_padding, + right: side_padding, + }, + }, + }, + active: { + default: { + background: background(layer, "active"), + padding: { + left: side_padding, + right: side_padding, + }, + }, + }, + }, + contact_avatar: { + corner_radius: 10, + width: 18, + }, + contact_status_free: { + corner_radius: 4, + padding: 4, + margin: { top: 12, left: 12 }, + background: foreground(layer, "positive"), + }, + contact_status_busy: { + corner_radius: 4, + padding: 4, + margin: { top: 12, left: 12 }, + background: foreground(layer, "negative"), + }, + contact_username: { + ...text(layer, "mono", { size: "sm" }), + margin: { + left: name_margin, + }, + }, + contact_button_spacing: name_margin, + contact_button: interactive({ + base: { ...contact_button }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), + disabled_button: { + ...contact_button, + background: background(layer, "on"), + color: foreground(layer, "on"), + }, + calling_indicator: { + ...text(layer, "mono", "variant", { size: "xs" }), + }, + tree_branch: toggleable({ + base: interactive({ + base: { + color: border_color(layer), + width: 1, + }, + state: { + hovered: { + color: border_color(layer), + }, + }, + }), + state: { + active: { + default: { + color: border_color(layer), + }, + }, + }, + }), + project_row: toggleable({ + base: interactive({ + base: { + ...project_row, + background: background(layer), + icon: { + margin: { left: name_margin }, + color: foreground(layer, "variant"), + width: 12, + }, + name: { + ...project_row.name, + ...text(layer, "mono", { size: "sm" }), + }, + }, + state: { + hovered: { + background: background(layer, "hovered"), + }, + }, + }), + state: { + active: { + default: { background: background(layer, "active") }, + }, + }, + }), + } +} diff --git a/styles/src/style_tree/contact_notification.ts b/styles/src/style_tree/contact_notification.ts new file mode 100644 index 0000000000..365e3a646d --- /dev/null +++ b/styles/src/style_tree/contact_notification.ts @@ -0,0 +1,55 @@ +import { background, foreground, text } from "./components" +import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function contact_notification(): any { + const theme = useTheme() + + const avatar_size = 12 + const header_padding = 8 + + return { + header_avatar: { + height: avatar_size, + width: avatar_size, + corner_radius: 6, + }, + header_message: { + ...text(theme.lowest, "sans", { size: "xs" }), + margin: { left: header_padding, right: header_padding }, + }, + header_height: 18, + body_message: { + ...text(theme.lowest, "sans", { size: "xs" }), + margin: { left: avatar_size + header_padding, top: 6, bottom: 6 }, + }, + button: interactive({ + base: { + ...text(theme.lowest, "sans", "on", { size: "xs" }), + background: background(theme.lowest, "on"), + padding: 4, + corner_radius: 6, + margin: { left: 6 }, + }, + + state: { + hovered: { + background: background(theme.lowest, "on", "hovered"), + }, + }, + }), + + dismiss_button: { + default: { + color: foreground(theme.lowest, "variant"), + icon_width: 8, + icon_height: 8, + button_width: 8, + button_height: 8, + hover: { + color: foreground(theme.lowest, "hovered"), + }, + }, + }, + } +} diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts new file mode 100644 index 0000000000..0ce63d088a --- /dev/null +++ b/styles/src/style_tree/contacts_popover.ts @@ -0,0 +1,16 @@ +import { useTheme } from "../theme" +import { background, border } from "./components" + +export default function contacts_popover(): any { + const theme = useTheme() + + return { + background: background(theme.middle), + corner_radius: 6, + padding: { top: 6, bottom: 6 }, + shadow: theme.popover_shadow, + border: border(theme.middle), + width: 300, + height: 400, + } +} diff --git a/styles/src/style_tree/context_menu.ts b/styles/src/style_tree/context_menu.ts new file mode 100644 index 0000000000..d4266a71fe --- /dev/null +++ b/styles/src/style_tree/context_menu.ts @@ -0,0 +1,70 @@ +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() + + return { + background: background(theme.middle), + corner_radius: 10, + padding: 4, + shadow: theme.popover_shadow, + border: border(theme.middle), + keystroke_margin: 30, + item: toggleable({ + base: interactive({ + base: { + icon_spacing: 8, + icon_width: 14, + padding: { left: 6, right: 6, top: 2, bottom: 2 }, + corner_radius: 6, + label: text(theme.middle, "sans", { size: "sm" }), + keystroke: { + ...text(theme.middle, "sans", "variant", { + size: "sm", + weight: "bold", + }), + padding: { left: 3, right: 3 }, + }, + }, + state: { + hovered: { + background: background(theme.middle, "hovered"), + label: text(theme.middle, "sans", "hovered", { + size: "sm", + }), + keystroke: { + ...text(theme.middle, "sans", "hovered", { + size: "sm", + weight: "bold", + }), + padding: { left: 3, right: 3 }, + }, + }, + clicked: { + background: background(theme.middle, "pressed"), + }, + }, + }), + state: { + active: { + default: { + background: background(theme.middle, "active"), + }, + hovered: { + background: background(theme.middle, "hovered"), + }, + clicked: { + background: background(theme.middle, "pressed"), + }, + }, + }, + }), + + separator: { + background: border_color(theme.middle), + margin: { top: 2, bottom: 2 }, + }, + } +} diff --git a/styles/src/style_tree/copilot.ts b/styles/src/style_tree/copilot.ts new file mode 100644 index 0000000000..f002db5ef5 --- /dev/null +++ b/styles/src/style_tree/copilot.ts @@ -0,0 +1,293 @@ +import { background, border, foreground, svg, text } from "./components" +import { interactive } from "../element" +import { useTheme } from "../theme" +export default function copilot(): any { + const theme = useTheme() + + const content_width = 264 + + const cta_button = + // Copied from welcome screen. FIXME: Move this into a ZDS component + interactive({ + base: { + background: background(theme.middle), + border: border(theme.middle, "default"), + corner_radius: 4, + margin: { + top: 4, + bottom: 4, + left: 8, + right: 8, + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text(theme.middle, "sans", "default", { size: "sm" }), + }, + state: { + hovered: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + background: background(theme.middle, "hovered"), + border: border(theme.middle, "active"), + }, + }, + }) + + return { + out_link_icon: interactive({ + base: { + icon: svg( + foreground(theme.middle, "variant"), + "icons/link_out_12.svg", + 12, + 12 + ), + container: { + corner_radius: 6, + padding: { left: 6 }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.middle, "hovered"), + }, + }, + }, + }), + + modal: { + title_text: { + default: { + ...text(theme.middle, "sans", { + size: "xs", + weight: "bold", + }), + }, + }, + titlebar: { + background: background(theme.lowest), + border: border(theme.middle, "active"), + padding: { + top: 4, + bottom: 4, + left: 8, + right: 8, + }, + }, + container: { + background: background(theme.lowest), + padding: { + top: 0, + left: 0, + right: 0, + bottom: 8, + }, + }, + close_icon: interactive({ + base: { + icon: svg( + foreground(theme.middle, "variant"), + "icons/x_mark_8.svg", + 8, + 8 + ), + container: { + corner_radius: 2, + padding: { + top: 4, + bottom: 4, + left: 4, + right: 4, + }, + margin: { + right: 0, + }, + }, + }, + state: { + hovered: { + icon: svg( + foreground(theme.middle, "on"), + "icons/x_mark_8.svg", + 8, + 8 + ), + }, + clicked: { + icon: svg( + foreground(theme.middle, "base"), + "icons/x_mark_8.svg", + 8, + 8 + ), + }, + }, + }), + dimensions: { + width: 280, + height: 280, + }, + }, + + auth: { + content_width, + + cta_button, + + header: { + icon: svg( + foreground(theme.middle, "default"), + "icons/zed_plus_copilot_32.svg", + 92, + 32 + ), + container: { + margin: { + top: 35, + bottom: 5, + left: 0, + right: 0, + }, + }, + }, + + prompting: { + subheading: { + ...text(theme.middle, "sans", { size: "xs" }), + margin: { + top: 6, + bottom: 12, + left: 0, + right: 0, + }, + }, + + hint: { + ...text(theme.middle, "sans", { + size: "xs", + color: "#838994", + }), + margin: { + top: 6, + bottom: 2, + }, + }, + + device_code: { + text: text(theme.middle, "mono", { size: "sm" }), + cta: { + ...cta_button, + background: background(theme.lowest), + border: border(theme.lowest, "inverted"), + padding: { + top: 0, + bottom: 0, + left: 16, + right: 16, + }, + margin: { + left: 16, + right: 16, + }, + }, + left: content_width / 2, + left_container: { + padding: { + top: 3, + bottom: 3, + left: 0, + right: 6, + }, + }, + right: (content_width * 1) / 3, + right_container: interactive({ + base: { + border: border(theme.lowest, "inverted", { + bottom: false, + right: false, + top: false, + left: true, + }), + padding: { + top: 3, + bottom: 5, + left: 8, + right: 0, + }, + }, + state: { + hovered: { + border: border(theme.middle, "active", { + bottom: false, + right: false, + top: false, + left: true, + }), + }, + }, + }), + }, + }, + + not_authorized: { + subheading: { + ...text(theme.middle, "sans", { size: "xs" }), + + margin: { + top: 16, + bottom: 16, + left: 0, + right: 0, + }, + }, + + warning: { + ...text(theme.middle, "sans", { + size: "xs", + color: foreground(theme.middle, "warning"), + }), + border: border(theme.middle, "warning"), + background: background(theme.middle, "warning"), + corner_radius: 2, + padding: { + top: 4, + left: 4, + bottom: 4, + right: 4, + }, + margin: { + bottom: 16, + left: 8, + right: 8, + }, + }, + }, + + authorized: { + subheading: { + ...text(theme.middle, "sans", { size: "xs" }), + + margin: { + top: 16, + bottom: 16, + }, + }, + + hint: { + ...text(theme.middle, "sans", { + size: "xs", + color: "#838994", + }), + margin: { + top: 24, + bottom: 4, + }, + }, + }, + }, + } +} diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts new file mode 100644 index 0000000000..a1ba0be43d --- /dev/null +++ b/styles/src/style_tree/editor.ts @@ -0,0 +1,319 @@ +import { with_opacity } from "../theme/color" +import { Layer, StyleSets } from "../theme/create_theme" +import { + background, + border, + border_color, + foreground, + text, +} from "./components" +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() + + const { is_light } = theme + + const layer = theme.highest + + const autocomplete_item = { + corner_radius: 6, + padding: { + bottom: 2, + left: 6, + right: 6, + top: 2, + }, + } + + function diagnostic(layer: Layer, style_set: StyleSets) { + return { + text_scale_factor: 0.857, + header: { + border: border(layer, { + top: true, + }), + }, + message: { + text: text(layer, "sans", style_set, "default", { size: "sm" }), + highlight_text: text(layer, "sans", style_set, "default", { + size: "sm", + weight: "bold", + }), + }, + } + } + + const syntax = build_syntax() + + return { + text_color: syntax.primary.color, + background: background(layer), + active_line_background: with_opacity(background(layer, "on"), 0.75), + highlighted_line_background: background(layer, "on"), + // Inline autocomplete suggestions, Co-pilot suggestions, etc. + hint: syntax.hint, + suggestion: syntax.predictive, + code_actions: { + indicator: toggleable({ + base: interactive({ + base: { + color: foreground(layer, "variant"), + }, + state: { + hovered: { + color: foreground(layer, "variant", "hovered"), + }, + clicked: { + color: foreground(layer, "variant", "pressed"), + }, + }, + }), + state: { + active: { + default: { + color: foreground(layer, "accent"), + }, + hovered: { + color: foreground(layer, "accent", "hovered"), + }, + clicked: { + color: foreground(layer, "accent", "pressed"), + }, + }, + }, + }), + + vertical_scale: 0.55, + }, + folds: { + icon_margin_scale: 2.5, + folded_icon: "icons/chevron_right_8.svg", + foldable_icon: "icons/chevron_down_8.svg", + indicator: toggleable({ + base: interactive({ + base: { + color: foreground(layer, "variant"), + }, + state: { + hovered: { + color: foreground(layer, "on"), + }, + clicked: { + color: foreground(layer, "base"), + }, + }, + }), + state: { + active: { + default: { + color: foreground(layer, "default"), + }, + hovered: { + color: foreground(layer, "variant"), + }, + }, + }, + }), + ellipses: { + text_color: theme.ramps.neutral(0.71).hex(), + corner_radius_factor: 0.15, + background: { + // Copied from hover_popover highlight + default: { + color: theme.ramps.neutral(0.5).alpha(0.0).hex(), + }, + + hovered: { + color: theme.ramps.neutral(0.5).alpha(0.5).hex(), + }, + + clicked: { + color: theme.ramps.neutral(0.5).alpha(0.7).hex(), + }, + }, + }, + fold_background: foreground(layer, "variant"), + }, + diff: { + deleted: is_light + ? theme.ramps.red(0.5).hex() + : theme.ramps.red(0.4).hex(), + modified: is_light + ? theme.ramps.yellow(0.5).hex() + : theme.ramps.yellow(0.5).hex(), + inserted: is_light + ? theme.ramps.green(0.4).hex() + : theme.ramps.green(0.5).hex(), + removed_width_em: 0.275, + width_em: 0.15, + corner_radius: 0.05, + }, + /** Highlights matching occurrences of what is under the cursor + * as well as matched brackets + */ + document_highlight_read_background: with_opacity( + foreground(layer, "accent"), + 0.1 + ), + document_highlight_write_background: theme.ramps + .neutral(0.5) + .alpha(0.4) + .hex(), // TODO: This was blend * 2 + error_color: background(layer, "negative"), + gutter_background: background(layer), + gutter_padding_factor: 3.5, + line_number: with_opacity(foreground(layer), 0.35), + line_number_active: foreground(layer), + rename_fade: 0.6, + unnecessary_code_fade: 0.5, + selection: theme.players[0], + whitespace: theme.ramps.neutral(0.5).hex(), + guest_selections: [ + theme.players[1], + theme.players[2], + theme.players[3], + theme.players[4], + theme.players[5], + theme.players[6], + theme.players[7], + ], + autocomplete: { + background: background(theme.middle), + corner_radius: 8, + padding: 4, + margin: { + left: -14, + }, + border: border(theme.middle), + shadow: theme.popover_shadow, + match_highlight: foreground(theme.middle, "accent"), + item: autocomplete_item, + hovered_item: { + ...autocomplete_item, + match_highlight: foreground(theme.middle, "accent", "hovered"), + background: background(theme.middle, "hovered"), + }, + selected_item: { + ...autocomplete_item, + match_highlight: foreground(theme.middle, "accent", "active"), + background: background(theme.middle, "active"), + }, + }, + diagnostic_header: { + background: background(theme.middle), + icon_width_factor: 1.5, + text_scale_factor: 0.857, + border: border(theme.middle, { + bottom: true, + top: true, + }), + code: { + ...text(theme.middle, "mono", { size: "sm" }), + margin: { + left: 10, + }, + }, + source: { + text: text(theme.middle, "sans", { + size: "sm", + weight: "bold", + }), + }, + message: { + highlight_text: text(theme.middle, "sans", { + size: "sm", + weight: "bold", + }), + text: text(theme.middle, "sans", { size: "sm" }), + }, + }, + diagnostic_path_header: { + background: background(theme.middle), + text_scale_factor: 0.857, + filename: text(theme.middle, "mono", { size: "sm" }), + path: { + ...text(theme.middle, "mono", { size: "sm" }), + margin: { + left: 12, + }, + }, + }, + error_diagnostic: diagnostic(theme.middle, "negative"), + warning_diagnostic: diagnostic(theme.middle, "warning"), + information_diagnostic: diagnostic(theme.middle, "accent"), + hint_diagnostic: diagnostic(theme.middle, "warning"), + invalid_error_diagnostic: diagnostic(theme.middle, "base"), + 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(), + link_definition: { + color: syntax.link_uri.color, + underline: syntax.link_uri.underline, + }, + jump_icon: interactive({ + base: { + color: foreground(layer, "on"), + icon_width: 20, + button_width: 20, + corner_radius: 6, + padding: { + top: 6, + bottom: 6, + left: 6, + right: 6, + }, + }, + state: { + hovered: { + background: background(layer, "on", "hovered"), + }, + }, + }), + + scrollbar: { + width: 12, + min_height_factor: 1.0, + track: { + border: border(layer, "variant", { left: true }), + }, + thumb: { + background: with_opacity(background(layer, "inverted"), 0.3), + border: { + width: 1, + color: border_color(layer, "variant"), + top: false, + right: true, + left: true, + bottom: false, + }, + }, + git: { + deleted: is_light + ? with_opacity(theme.ramps.red(0.5).hex(), 0.8) + : with_opacity(theme.ramps.red(0.4).hex(), 0.8), + modified: is_light + ? with_opacity(theme.ramps.yellow(0.5).hex(), 0.8) + : with_opacity(theme.ramps.yellow(0.4).hex(), 0.8), + inserted: is_light + ? with_opacity(theme.ramps.green(0.5).hex(), 0.8) + : with_opacity(theme.ramps.green(0.4).hex(), 0.8), + }, + selections: is_light + ? with_opacity(theme.ramps.blue(0.5).hex(), 0.8) + : with_opacity(theme.ramps.blue(0.4).hex(), 0.8) + }, + composition_mark: { + underline: { + thickness: 1.0, + color: border_color(layer), + }, + }, + syntax, + } +} diff --git a/styles/src/style_tree/feedback.ts b/styles/src/style_tree/feedback.ts new file mode 100644 index 0000000000..2bb63e951b --- /dev/null +++ b/styles/src/style_tree/feedback.ts @@ -0,0 +1,51 @@ +import { background, border, text } from "./components" +import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function feedback(): any { + const theme = useTheme() + + return { + submit_button: interactive({ + base: { + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + corner_radius: 6, + border: border(theme.highest, "on"), + margin: { + right: 4, + }, + padding: { + bottom: 2, + left: 10, + right: 10, + top: 2, + }, + }, + state: { + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + }, + }), + button_margin: 8, + info_text_default: text(theme.highest, "sans", "default", { + size: "xs", + }), + link_text_default: text(theme.highest, "sans", "default", { + size: "xs", + underline: true, + }), + link_text_hover: text(theme.highest, "sans", "hovered", { + size: "xs", + underline: true, + }), + } +} diff --git a/styles/src/style_tree/hover_popover.ts b/styles/src/style_tree/hover_popover.ts new file mode 100644 index 0000000000..80f2250349 --- /dev/null +++ b/styles/src/style_tree/hover_popover.ts @@ -0,0 +1,49 @@ +import { useTheme } from "../theme" +import { background, border, foreground, text } from "./components" + +export default function hover_popover(): any { + const theme = useTheme() + + const base_container = { + background: background(theme.middle), + corner_radius: 8, + padding: { + left: 8, + right: 8, + top: 4, + bottom: 4, + }, + shadow: theme.popover_shadow, + border: border(theme.middle), + margin: { + left: -8, + }, + } + + return { + container: base_container, + info_container: { + ...base_container, + background: background(theme.middle, "accent"), + border: border(theme.middle, "accent"), + }, + warning_container: { + ...base_container, + background: background(theme.middle, "warning"), + border: border(theme.middle, "warning"), + }, + error_container: { + ...base_container, + background: background(theme.middle, "negative"), + border: border(theme.middle, "negative"), + }, + block_style: { + padding: { top: 4 }, + }, + prose: text(theme.middle, "sans", { size: "sm" }), + diagnostic_source_highlight: { + color: foreground(theme.middle, "accent"), + }, + highlight: theme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better + } +} diff --git a/styles/src/style_tree/incoming_call_notification.ts b/styles/src/style_tree/incoming_call_notification.ts new file mode 100644 index 0000000000..294ec00a73 --- /dev/null +++ b/styles/src/style_tree/incoming_call_notification.ts @@ -0,0 +1,55 @@ +import { useTheme } from "../theme" +import { background, border, text } from "./components" + +export default function incoming_call_notification(): unknown { + const theme = useTheme() + + const avatar_size = 48 + return { + window_height: 74, + window_width: 380, + background: background(theme.middle), + caller_container: { + padding: 12, + }, + caller_avatar: { + height: avatar_size, + width: avatar_size, + corner_radius: avatar_size / 2, + }, + caller_metadata: { + margin: { left: 10 }, + }, + caller_username: { + ...text(theme.middle, "sans", { size: "sm", weight: "bold" }), + margin: { top: -3 }, + }, + caller_message: { + ...text(theme.middle, "sans", "variant", { size: "xs" }), + margin: { top: -3 }, + }, + worktree_roots: { + ...text(theme.middle, "sans", "variant", { + size: "xs", + weight: "bold", + }), + margin: { top: -3 }, + }, + button_width: 96, + accept_button: { + background: background(theme.middle, "accent"), + border: border(theme.middle, { left: true, bottom: true }), + ...text(theme.middle, "sans", "positive", { + size: "xs", + weight: "bold", + }), + }, + decline_button: { + border: border(theme.middle, { left: true }), + ...text(theme.middle, "sans", "negative", { + size: "xs", + weight: "bold", + }), + }, + } +} diff --git a/styles/src/style_tree/picker.ts b/styles/src/style_tree/picker.ts new file mode 100644 index 0000000000..bbd664397f --- /dev/null +++ b/styles/src/style_tree/picker.ts @@ -0,0 +1,132 @@ +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() + + const container = { + background: background(theme.lowest), + border: border(theme.lowest), + shadow: theme.modal_shadow, + corner_radius: 12, + padding: { + bottom: 4, + }, + } + const input_editor = { + placeholder_text: text(theme.lowest, "sans", "on", "disabled"), + selection: theme.players[0], + text: text(theme.lowest, "mono", "on"), + border: border(theme.lowest, { bottom: true }), + padding: { + bottom: 8, + left: 16, + right: 16, + top: 8, + }, + margin: { + bottom: 4, + }, + } + const empty_input_editor: any = { ...input_editor } + delete empty_input_editor.border + delete empty_input_editor.margin + + return { + ...container, + empty_container: { + ...container, + padding: {}, + }, + item: toggleable({ + base: interactive({ + base: { + padding: { + bottom: 4, + left: 12, + right: 12, + top: 4, + }, + margin: { + top: 1, + left: 4, + right: 4, + }, + corner_radius: 8, + text: text(theme.lowest, "sans", "variant"), + highlight_text: text(theme.lowest, "sans", "accent", { + weight: "bold", + }), + }, + state: { + hovered: { + background: with_opacity( + background(theme.lowest, "hovered"), + 0.5 + ), + }, + clicked: { + background: with_opacity( + background(theme.lowest, "pressed"), + 0.5 + ), + }, + }, + }), + state: { + active: { + default: { + background: with_opacity( + background(theme.lowest, "base", "active"), + 0.5 + ), + }, + hovered: { + background: with_opacity( + background(theme.lowest, "hovered"), + 0.5 + ), + }, + clicked: { + background: with_opacity( + background(theme.lowest, "pressed"), + 0.5 + ), + }, + }, + }, + }), + + input_editor, + empty_input_editor, + no_matches: { + text: text(theme.lowest, "sans", "variant"), + padding: { + bottom: 8, + left: 16, + right: 16, + 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, + }, + + } + } +} diff --git a/styles/src/style_tree/project_diagnostics.ts b/styles/src/style_tree/project_diagnostics.ts new file mode 100644 index 0000000000..1c13b31a4a --- /dev/null +++ b/styles/src/style_tree/project_diagnostics.ts @@ -0,0 +1,14 @@ +import { useTheme } from "../theme" +import { background, text } from "./components" + +export default function project_diagnostics(): any { + const theme = useTheme() + + return { + background: background(theme.highest), + tab_icon_spacing: 4, + tab_icon_width: 13, + tab_summary_spacing: 10, + empty_message: text(theme.highest, "sans", "variant", { size: "md" }), + } +} diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts new file mode 100644 index 0000000000..af997d0a6e --- /dev/null +++ b/styles/src/style_tree/project_panel.ts @@ -0,0 +1,199 @@ +import { with_opacity } from "../theme/color" +import { + Border, + TextStyle, + background, + border, + foreground, + text, +} from "./components" +import { interactive, toggleable } from "../element" +import merge from "ts-deepmerge" +import { useTheme } from "../theme" +export default function project_panel(): any { + const theme = useTheme() + + const { is_light } = theme + + type EntryStateProps = { + background?: string + border?: Border + text?: TextStyle + icon_color?: string + } + + type EntryState = { + default: EntryStateProps + hovered?: EntryStateProps + clicked?: EntryStateProps + } + + const entry = (unselected?: EntryState, selected?: EntryState) => { + const git_status = { + git: { + modified: is_light + ? theme.ramps.yellow(0.6).hex() + : theme.ramps.yellow(0.5).hex(), + inserted: is_light + ? theme.ramps.green(0.45).hex() + : theme.ramps.green(0.5).hex(), + conflict: is_light + ? theme.ramps.red(0.6).hex() + : theme.ramps.red(0.5).hex(), + }, + } + + const base_properties = { + height: 22, + background: background(theme.middle), + icon_color: foreground(theme.middle, "variant"), + icon_size: 7, + icon_spacing: 5, + text: text(theme.middle, "sans", "variant", { size: "sm" }), + status: { + ...git_status, + }, + } + + const selected_style: EntryState | undefined = selected + ? selected + : unselected + + const unselected_default_style = merge( + base_properties, + unselected?.default ?? {}, + {} + ) + const unselected_hovered_style = merge( + base_properties, + { background: background(theme.middle, "hovered") }, + unselected?.hovered ?? {} + ) + const unselected_clicked_style = merge( + base_properties, + { background: background(theme.middle, "pressed") }, + unselected?.clicked ?? {} + ) + const selected_default_style = merge( + base_properties, + { + background: background(theme.lowest), + text: text(theme.lowest, "sans", { size: "sm" }), + }, + 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 ?? {} + ) + const selected_clicked_style = merge( + base_properties, + { + background: background(theme.lowest, "pressed"), + text: text(theme.lowest, "sans", { size: "sm" }), + }, + selected_style?.clicked ?? {} + ) + + return toggleable({ + state: { + inactive: interactive({ + state: { + default: unselected_default_style, + hovered: unselected_hovered_style, + clicked: unselected_clicked_style, + }, + }), + active: interactive({ + state: { + default: selected_default_style, + hovered: selected_hovered_style, + clicked: selected_clicked_style, + }, + }), + }, + }) + } + + const default_entry = entry() + + return { + open_project_button: interactive({ + base: { + background: background(theme.middle), + border: border(theme.middle, "active"), + corner_radius: 4, + margin: { + top: 16, + left: 16, + right: 16, + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text(theme.middle, "sans", "default", { size: "sm" }), + }, + state: { + hovered: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + background: background(theme.middle, "hovered"), + border: border(theme.middle, "active"), + }, + clicked: { + ...text(theme.middle, "sans", "default", { size: "sm" }), + background: background(theme.middle, "pressed"), + border: border(theme.middle, "active"), + }, + }, + }), + background: background(theme.middle), + padding: { left: 6, right: 6, top: 0, bottom: 6 }, + indent_width: 12, + entry: default_entry, + dragged_entry: { + ...default_entry.inactive.default, + text: text(theme.middle, "sans", "on", { size: "sm" }), + background: with_opacity(background(theme.middle, "on"), 0.9), + border: border(theme.middle), + }, + ignored_entry: entry( + { + default: { + text: text(theme.middle, "sans", "disabled"), + }, + }, + { + default: { + icon_color: foreground(theme.middle, "variant"), + }, + } + ), + cut_entry: entry( + { + default: { + text: text(theme.middle, "sans", "disabled"), + }, + }, + { + default: { + background: background(theme.middle, "active"), + text: text(theme.middle, "sans", "disabled", { + size: "sm", + }), + }, + } + ), + filename_editor: { + background: background(theme.middle, "on"), + text: text(theme.middle, "sans", "on", { size: "sm" }), + selection: theme.players[0], + }, + } +} diff --git a/styles/src/style_tree/project_shared_notification.ts b/styles/src/style_tree/project_shared_notification.ts new file mode 100644 index 0000000000..e7c1dcedd5 --- /dev/null +++ b/styles/src/style_tree/project_shared_notification.ts @@ -0,0 +1,55 @@ +import { useTheme } from "../theme" +import { background, border, text } from "./components" + +export default function project_shared_notification(): unknown { + const theme = useTheme() + + const avatar_size = 48 + return { + window_height: 74, + window_width: 380, + background: background(theme.middle), + owner_container: { + padding: 12, + }, + owner_avatar: { + height: avatar_size, + width: avatar_size, + corner_radius: avatar_size / 2, + }, + owner_metadata: { + margin: { left: 10 }, + }, + owner_username: { + ...text(theme.middle, "sans", { size: "sm", weight: "bold" }), + margin: { top: -3 }, + }, + message: { + ...text(theme.middle, "sans", "variant", { size: "xs" }), + margin: { top: -3 }, + }, + worktree_roots: { + ...text(theme.middle, "sans", "variant", { + size: "xs", + weight: "bold", + }), + margin: { top: -3 }, + }, + button_width: 96, + open_button: { + background: background(theme.middle, "accent"), + border: border(theme.middle, { left: true, bottom: true }), + ...text(theme.middle, "sans", "accent", { + size: "xs", + weight: "bold", + }), + }, + dismiss_button: { + border: border(theme.middle, { left: true }), + ...text(theme.middle, "sans", "variant", { + size: "xs", + weight: "bold", + }), + }, + } +} diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts new file mode 100644 index 0000000000..5c16d03233 --- /dev/null +++ b/styles/src/style_tree/search.ts @@ -0,0 +1,138 @@ +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() + + // Search input + const editor = { + background: background(theme.highest), + corner_radius: 8, + min_width: 200, + max_width: 500, + placeholder_text: text(theme.highest, "mono", "disabled"), + selection: theme.players[0], + text: text(theme.highest, "mono", "default"), + border: border(theme.highest), + margin: { + right: 12, + }, + padding: { + top: 3, + bottom: 3, + left: 12, + right: 8, + }, + } + + const include_exclude_editor = { + ...editor, + min_width: 100, + max_width: 250, + } + + return { + // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive + match_background: with_opacity( + foreground(theme.highest, "accent"), + 0.4 + ), + option_button: toggleable({ + base: interactive({ + base: { + ...text(theme.highest, "mono", "on"), + background: background(theme.highest, "on"), + corner_radius: 6, + border: border(theme.highest, "on"), + margin: { + right: 4, + }, + padding: { + bottom: 2, + left: 10, + right: 10, + top: 2, + }, + }, + state: { + hovered: { + ...text(theme.highest, "mono", "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + border: border(theme.highest, "on", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "on", "pressed"), + background: background(theme.highest, "on", "pressed"), + border: border(theme.highest, "on", "pressed"), + }, + }, + }), + state: { + active: { + default: { + ...text(theme.highest, "mono", "accent"), + }, + hovered: { + ...text(theme.highest, "mono", "accent", "hovered"), + }, + clicked: { + ...text(theme.highest, "mono", "accent", "pressed"), + }, + }, + }, + }), + editor, + invalid_editor: { + ...editor, + border: border(theme.highest, "negative"), + }, + include_exclude_editor, + invalid_include_exclude_editor: { + ...include_exclude_editor, + border: border(theme.highest, "negative"), + }, + match_index: { + ...text(theme.highest, "mono", "variant"), + padding: { + left: 6, + }, + }, + option_button_group: { + padding: { + left: 12, + right: 12, + }, + }, + include_exclude_inputs: { + ...text(theme.highest, "mono", "variant"), + padding: { + right: 6, + }, + }, + results_status: { + ...text(theme.highest, "mono", "on"), + size: 18, + }, + dismiss_button: interactive({ + base: { + color: foreground(theme.highest, "variant"), + icon_width: 12, + button_width: 14, + padding: { + left: 10, + right: 10, + }, + }, + state: { + hovered: { + color: foreground(theme.highest, "hovered"), + }, + clicked: { + color: foreground(theme.highest, "pressed"), + }, + }, + }), + } +} diff --git a/styles/src/style_tree/shared_screen.ts b/styles/src/style_tree/shared_screen.ts new file mode 100644 index 0000000000..aca7fd7f07 --- /dev/null +++ b/styles/src/style_tree/shared_screen.ts @@ -0,0 +1,10 @@ +import { useTheme } from "../theme" +import { background } from "./components" + +export default function sharedScreen() { + const theme = useTheme() + + return { + background: background(theme.highest), + } +} diff --git a/styles/src/style_tree/simple_message_notification.ts b/styles/src/style_tree/simple_message_notification.ts new file mode 100644 index 0000000000..35133f04a2 --- /dev/null +++ b/styles/src/style_tree/simple_message_notification.ts @@ -0,0 +1,52 @@ +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() + + const header_padding = 8 + + return { + message: { + ...text(theme.middle, "sans", { size: "xs" }), + margin: { left: header_padding, right: header_padding }, + }, + action_message: interactive({ + base: { + ...text(theme.middle, "sans", { size: "xs" }), + border: border(theme.middle, "active"), + corner_radius: 4, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + + margin: { left: header_padding, top: 6, bottom: 6 }, + }, + state: { + hovered: { + ...text(theme.middle, "sans", "default", { size: "xs" }), + background: background(theme.middle, "hovered"), + border: border(theme.middle, "active"), + }, + }, + }), + dismiss_button: interactive({ + base: { + color: foreground(theme.middle), + icon_width: 8, + icon_height: 8, + button_width: 8, + button_height: 8, + }, + state: { + hovered: { + color: foreground(theme.middle, "hovered"), + }, + }, + }), + } +} diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts new file mode 100644 index 0000000000..9aeea866f3 --- /dev/null +++ b/styles/src/style_tree/status_bar.ts @@ -0,0 +1,156 @@ +import { background, border, foreground, text } from "./components" +import { interactive, toggleable } from "../element" +import { useTheme } from "../common" +export default function status_bar(): any { + const theme = useTheme() + + const layer = theme.lowest + + const status_container = { + corner_radius: 6, + padding: { top: 3, bottom: 3, left: 6, right: 6 }, + } + + const diagnostic_status_container = { + corner_radius: 6, + padding: { top: 1, bottom: 1, left: 6, right: 6 }, + } + + return { + height: 30, + item_spacing: 8, + padding: { + top: 1, + bottom: 1, + left: 6, + right: 6, + }, + border: border(layer, { top: true, overlay: true }), + cursor_position: text(layer, "sans", "variant"), + active_language: interactive({ + base: { + padding: { left: 6, right: 6 }, + ...text(layer, "sans", "variant"), + }, + state: { + hovered: { + ...text(layer, "sans", "on"), + }, + }, + }), + auto_update_progress_message: text(layer, "sans", "variant"), + auto_update_done_message: text(layer, "sans", "variant"), + lsp_status: interactive({ + base: { + ...diagnostic_status_container, + icon_spacing: 4, + icon_width: 14, + height: 18, + message: text(layer, "sans"), + icon_color: foreground(layer), + }, + state: { + hovered: { + message: text(layer, "sans"), + icon_color: foreground(layer), + background: background(layer, "hovered"), + }, + }, + }), + diagnostic_message: interactive({ + base: { + ...text(layer, "sans"), + }, + state: { hovered: text(layer, "sans", "hovered") }, + }), + diagnostic_summary: interactive({ + base: { + height: 20, + icon_width: 16, + icon_spacing: 2, + summary_spacing: 6, + text: text(layer, "sans", { size: "sm" }), + icon_color_ok: foreground(layer, "variant"), + icon_color_warning: foreground(layer, "warning"), + icon_color_error: foreground(layer, "negative"), + container_ok: { + corner_radius: 6, + padding: { top: 3, bottom: 3, left: 7, right: 7 }, + }, + container_warning: { + ...diagnostic_status_container, + background: background(layer, "warning"), + border: border(layer, "warning"), + }, + container_error: { + ...diagnostic_status_container, + background: background(layer, "negative"), + border: border(layer, "negative"), + }, + }, + state: { + hovered: { + icon_color_ok: foreground(layer, "on"), + container_ok: { + background: background(layer, "on", "hovered"), + }, + container_warning: { + background: background(layer, "warning", "hovered"), + border: border(layer, "warning", "hovered"), + }, + container_error: { + background: background(layer, "negative", "hovered"), + border: border(layer, "negative", "hovered"), + }, + }, + }, + }), + panel_buttons: { + group_left: {}, + group_bottom: {}, + group_right: {}, + button: toggleable({ + base: interactive({ + base: { + ...status_container, + icon_size: 16, + icon_color: foreground(layer, "variant"), + label: { + margin: { left: 6 }, + ...text(layer, "sans", { size: "sm" }), + }, + }, + state: { + hovered: { + icon_color: foreground(layer, "hovered"), + background: background(layer, "variant"), + }, + }, + }), + state: { + active: { + default: { + icon_color: foreground(layer, "active"), + background: background(layer, "active"), + }, + hovered: { + icon_color: foreground(layer, "hovered"), + background: background(layer, "hovered"), + }, + clicked: { + icon_color: foreground(layer, "pressed"), + background: background(layer, "pressed"), + }, + }, + }, + }), + badge: { + corner_radius: 3, + padding: 2, + margin: { bottom: -1, right: -1 }, + border: border(layer), + background: background(layer, "accent"), + }, + }, + } +} diff --git a/styles/src/style_tree/tab_bar.ts b/styles/src/style_tree/tab_bar.ts new file mode 100644 index 0000000000..29769f9bae --- /dev/null +++ b/styles/src/style_tree/tab_bar.ts @@ -0,0 +1,131 @@ +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() + + const height = 32 + + const active_layer = theme.highest + const layer = theme.middle + + const tab = { + height, + text: text(layer, "sans", "variant", { size: "sm" }), + background: background(layer), + border: border(layer, { + right: true, + bottom: true, + overlay: true, + }), + padding: { + left: 8, + right: 12, + }, + spacing: 8, + + // Tab type icons (e.g. Project Search) + type_icon_width: 14, + + // Close icons + close_icon_width: 8, + icon_close: foreground(layer, "variant"), + icon_close_active: foreground(layer, "hovered"), + + // Indicators + icon_conflict: foreground(layer, "warning"), + icon_dirty: foreground(layer, "accent"), + + // When two tabs of the same name are open, a label appears next to them + description: { + margin: { left: 8 }, + ...text(layer, "sans", "disabled", { size: "2xs" }), + }, + } + + const active_pane_active_tab = { + ...tab, + background: background(active_layer), + text: text(active_layer, "sans", "active", { size: "sm" }), + border: { + ...tab.border, + bottom: false, + }, + } + + const inactive_pane_inactive_tab = { + ...tab, + background: background(layer), + text: text(layer, "sans", "variant", { size: "sm" }), + } + + const inactive_pane_active_tab = { + ...tab, + background: background(active_layer), + text: text(layer, "sans", "variant", { size: "sm" }), + border: { + ...tab.border, + bottom: false, + }, + } + + const dragged_tab = { + ...active_pane_active_tab, + background: with_opacity(tab.background, 0.9), + border: undefined as any, + shadow: theme.popover_shadow, + } + + return { + height, + background: background(layer), + active_pane: { + active_tab: active_pane_active_tab, + inactive_tab: tab, + }, + inactive_pane: { + active_tab: inactive_pane_active_tab, + inactive_tab: inactive_pane_inactive_tab, + }, + dragged_tab, + pane_button: toggleable({ + base: interactive({ + base: { + color: foreground(layer, "variant"), + icon_width: 12, + button_width: active_pane_active_tab.height, + }, + state: { + hovered: { + color: foreground(layer, "hovered"), + }, + clicked: { + color: foreground(layer, "pressed"), + }, + }, + }), + state: { + active: { + default: { + color: foreground(layer, "accent"), + }, + hovered: { + color: foreground(layer, "hovered"), + }, + clicked: { + color: foreground(layer, "pressed"), + }, + }, + }, + }), + pane_button_container: { + background: tab.background, + border: { + ...tab.border, + right: false, + }, + }, + } +} diff --git a/styles/src/style_tree/terminal.ts b/styles/src/style_tree/terminal.ts new file mode 100644 index 0000000000..5b98eebfcd --- /dev/null +++ b/styles/src/style_tree/terminal.ts @@ -0,0 +1,54 @@ +import { useTheme } from "../theme" + +export default function terminal() { + const theme = useTheme() + + /** + * Colors are controlled per-cell in the terminal grid. + * Cells can be set to any of these more 'theme-capable' colors + * or can be set directly with RGB values. + * Here are the common interpretations of these names: + * https://en.wikipedia.org/wiki/ANSI_escape_code#Colors + */ + return { + black: theme.ramps.neutral(0).hex(), + red: theme.ramps.red(0.5).hex(), + green: theme.ramps.green(0.5).hex(), + yellow: theme.ramps.yellow(0.5).hex(), + blue: theme.ramps.blue(0.5).hex(), + magenta: theme.ramps.magenta(0.5).hex(), + cyan: theme.ramps.cyan(0.5).hex(), + white: theme.ramps.neutral(1).hex(), + bright_black: theme.ramps.neutral(0.4).hex(), + bright_red: theme.ramps.red(0.25).hex(), + bright_green: theme.ramps.green(0.25).hex(), + bright_yellow: theme.ramps.yellow(0.25).hex(), + bright_blue: theme.ramps.blue(0.25).hex(), + bright_magenta: theme.ramps.magenta(0.25).hex(), + bright_cyan: theme.ramps.cyan(0.25).hex(), + bright_white: theme.ramps.neutral(1).hex(), + /** + * Default color for characters + */ + foreground: theme.ramps.neutral(1).hex(), + /** + * Default color for the rectangle background of a cell + */ + background: theme.ramps.neutral(0).hex(), + modal_background: theme.ramps.neutral(0.1).hex(), + /** + * Default color for the cursor + */ + cursor: theme.players[0].cursor, + dim_black: theme.ramps.neutral(1).hex(), + dim_red: theme.ramps.red(0.75).hex(), + dim_green: theme.ramps.green(0.75).hex(), + dim_yellow: theme.ramps.yellow(0.75).hex(), + dim_blue: theme.ramps.blue(0.75).hex(), + dim_magenta: theme.ramps.magenta(0.75).hex(), + dim_cyan: theme.ramps.cyan(0.75).hex(), + dim_white: theme.ramps.neutral(0.6).hex(), + bright_foreground: theme.ramps.neutral(1).hex(), + dim_foreground: theme.ramps.neutral(0).hex(), + } +} diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts new file mode 100644 index 0000000000..60894b08f6 --- /dev/null +++ b/styles/src/style_tree/titlebar.ts @@ -0,0 +1,278 @@ +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" + +const ITEM_SPACING = 8 +const TITLEBAR_HEIGHT = 32 + +function build_spacing( + container_height: number, + element_height: number, + spacing: number +) { + return { + group: spacing, + item: spacing / 2, + half_item: spacing / 4, + margin_y: (container_height - element_height) / 2, + margin_x: (container_height - element_height) / 2, + } +} + +function call_controls() { + const theme = useTheme() + + const button_height = 18 + + const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING) + const margin_y = { + top: space.margin_y, + bottom: space.margin_y, + } + + return { + toggle_microphone_button: toggleable_icon_button(theme, { + margin: { + ...margin_y, + left: space.group, + right: space.half_item, + }, + active_color: "negative", + }), + + toggle_speakers_button: toggleable_icon_button(theme, { + margin: { + ...margin_y, + left: space.half_item, + right: space.half_item, + }, + }), + + screen_share_button: toggleable_icon_button(theme, { + margin: { + ...margin_y, + left: space.half_item, + right: space.group, + }, + active_color: "accent", + }), + + muted: foreground(theme.lowest, "negative"), + speaking: foreground(theme.lowest, "accent"), + } +} + +/** + * Opens the User Menu when toggled + * + * When logged in shows the user's avatar and a chevron, + * When logged out only shows a chevron. + */ +function user_menu() { + const theme = useTheme() + + const button_height = 18 + + const space = build_spacing(TITLEBAR_HEIGHT, button_height, ITEM_SPACING) + + const build_button = ({ online }: { online: boolean }) => { + const button = toggleable({ + base: interactive({ + base: { + corner_radius: 6, + height: button_height, + width: online ? 37 : 24, + padding: { + top: 2, + bottom: 2, + left: 6, + right: 6, + }, + margin: { + left: space.item, + right: space.item, + }, + ...text(theme.lowest, "sans", { size: "xs" }), + background: background(theme.lowest), + }, + state: { + hovered: { + ...text(theme.lowest, "sans", "hovered", { + size: "xs", + }), + background: background(theme.lowest, "hovered"), + }, + clicked: { + ...text(theme.lowest, "sans", "pressed", { + size: "xs", + }), + background: background(theme.lowest, "pressed"), + }, + }, + }), + state: { + active: { + default: { + ...text(theme.lowest, "sans", "active", { size: "xs" }), + background: background(theme.middle), + }, + hovered: { + ...text(theme.lowest, "sans", "active", { size: "xs" }), + background: background(theme.middle, "hovered"), + }, + clicked: { + ...text(theme.lowest, "sans", "active", { size: "xs" }), + background: background(theme.middle, "pressed"), + }, + }, + }, + }) + + return { + user_menu: button, + avatar: { + icon_width: 16, + icon_height: 16, + corner_radius: 4, + outer_width: 16, + outer_corner_radius: 16, + }, + icon: { + margin: { + top: 2, + left: online ? space.item : 0, + right: space.group, + bottom: 2, + }, + width: 11, + height: 11, + color: foreground(theme.lowest), + }, + } + } + return { + user_menu_button_online: build_button({ online: true }), + user_menu_button_offline: build_button({ online: false }), + } +} + +export function titlebar(): any { + const theme = useTheme() + + const avatar_width = 15 + const avatar_outer_width = avatar_width + 4 + const follower_avatar_width = 14 + const follower_avatar_outer_width = follower_avatar_width + 4 + + return { + item_spacing: ITEM_SPACING, + face_pile_spacing: 2, + height: TITLEBAR_HEIGHT, + background: background(theme.lowest), + border: border(theme.lowest, { bottom: true }), + padding: { + left: 80, + right: 0, + }, + + // Project + 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: { + width: avatar_width, + outer_width: avatar_outer_width, + corner_radius: avatar_width / 2, + outer_corner_radius: avatar_outer_width / 2, + }, + follower_avatar: { + width: follower_avatar_width, + outer_width: follower_avatar_outer_width, + corner_radius: follower_avatar_width / 2, + outer_corner_radius: follower_avatar_outer_width / 2, + }, + inactive_avatar_grayscale: true, + follower_avatar_overlap: 8, + leader_selection: { + margin: { + top: 4, + bottom: 4, + }, + padding: { + left: 2, + right: 2, + top: 2, + bottom: 2, + }, + corner_radius: 6, + }, + avatar_ribbon: { + height: 3, + width: 14, + // TODO: Chore: Make avatarRibbon colors driven by the theme rather than being hard coded. + }, + + sign_in_button: toggleable_text_button(theme, {}), + offline_icon: { + color: foreground(theme.lowest, "variant"), + width: 16, + margin: { + left: ITEM_SPACING, + }, + padding: { + right: 4, + }, + }, + + // When the collaboration server is out of date, show a warning + outdated_warning: { + ...text(theme.lowest, "sans", "warning", { size: "xs" }), + background: with_opacity(background(theme.lowest, "warning"), 0.3), + border: border(theme.lowest, "warning"), + margin: { + left: ITEM_SPACING, + }, + padding: { + left: 8, + right: 8, + }, + corner_radius: 6, + }, + + leave_call_button: icon_button({ + margin: { + left: ITEM_SPACING / 2, + right: ITEM_SPACING, + }, + }), + + ...call_controls(), + + toggle_contacts_button: toggleable_icon_button(theme, { + margin: { + left: ITEM_SPACING, + }, + }), + + // Jewel that notifies you that there are new contact requests + toggle_contacts_badge: { + corner_radius: 3, + padding: 2, + margin: { top: 3, left: 3 }, + border: border(theme.lowest), + background: foreground(theme.lowest, "accent"), + }, + share_button: toggleable_text_button(theme, {}), + user_menu: user_menu(), + } +} diff --git a/styles/src/style_tree/toolbar_dropdown_menu.ts b/styles/src/style_tree/toolbar_dropdown_menu.ts new file mode 100644 index 0000000000..97f29ab18c --- /dev/null +++ b/styles/src/style_tree/toolbar_dropdown_menu.ts @@ -0,0 +1,66 @@ +import { background, border, text } from "./components" +import { interactive, toggleable } from "../element" +import { useTheme } from "../theme" +export default function dropdown_menu(): any { + const theme = useTheme() + + return { + row_height: 30, + background: background(theme.middle), + border: border(theme.middle), + shadow: theme.popover_shadow, + header: interactive({ + base: { + ...text(theme.middle, "sans", { size: "sm" }), + secondary_text: text(theme.middle, "sans", { + size: "sm", + color: "#aaaaaa", + }), + secondary_text_spacing: 10, + padding: { left: 8, right: 8, top: 2, bottom: 2 }, + corner_radius: 6, + background: background(theme.middle, "on"), + }, + state: { + hovered: { + background: background(theme.middle, "hovered"), + }, + clicked: { + background: background(theme.middle, "pressed"), + }, + }, + }), + section_header: { + ...text(theme.middle, "sans", { size: "sm" }), + padding: { left: 8, right: 8, top: 8, bottom: 8 }, + }, + item: toggleable({ + base: interactive({ + base: { + ...text(theme.middle, "sans", { size: "sm" }), + secondary_text_spacing: 10, + secondary_text: text(theme.middle, "sans", { size: "sm" }), + padding: { left: 18, right: 18, top: 2, bottom: 2 }, + }, + state: { + hovered: { + background: background(theme.middle, "hovered"), + ...text(theme.middle, "sans", "hovered", { + size: "sm", + }), + }, + }, + }), + state: { + active: { + default: { + background: background(theme.middle, "active"), + }, + hovered: { + background: background(theme.middle, "hovered"), + }, + }, + }, + }), + } +} diff --git a/styles/src/style_tree/tooltip.ts b/styles/src/style_tree/tooltip.ts new file mode 100644 index 0000000000..54a2d7b78d --- /dev/null +++ b/styles/src/style_tree/tooltip.ts @@ -0,0 +1,24 @@ +import { useTheme } from "../theme" +import { background, border, text } from "./components" + +export default function tooltip(): any { + const theme = useTheme() + + return { + background: background(theme.middle), + border: border(theme.middle), + padding: { top: 4, bottom: 4, left: 8, right: 8 }, + margin: { top: 6, left: 6 }, + shadow: theme.popover_shadow, + corner_radius: 6, + text: text(theme.middle, "sans", { size: "xs" }), + keystroke: { + background: background(theme.middle, "on"), + corner_radius: 4, + margin: { left: 6 }, + padding: { left: 4, right: 4 }, + ...text(theme.middle, "mono", "on", { size: "xs", weight: "bold" }), + }, + max_text_width: 200, + } +} diff --git a/styles/src/style_tree/update_notification.ts b/styles/src/style_tree/update_notification.ts new file mode 100644 index 0000000000..2d0c36d74c --- /dev/null +++ b/styles/src/style_tree/update_notification.ts @@ -0,0 +1,41 @@ +import { foreground, text } from "./components" +import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function update_notification(): any { + const theme = useTheme() + + const header_padding = 8 + + return { + message: { + ...text(theme.middle, "sans", { size: "xs" }), + margin: { left: header_padding, right: header_padding }, + }, + action_message: interactive({ + base: { + ...text(theme.middle, "sans", { size: "xs" }), + margin: { left: header_padding, top: 6, bottom: 6 }, + }, + state: { + hovered: { + color: foreground(theme.middle, "hovered"), + }, + }, + }), + dismiss_button: interactive({ + base: { + color: foreground(theme.middle), + icon_width: 8, + icon_height: 8, + button_width: 8, + button_height: 8, + }, + state: { + hovered: { + color: foreground(theme.middle, "hovered"), + }, + }, + }), + } +} diff --git a/styles/src/style_tree/welcome.ts b/styles/src/style_tree/welcome.ts new file mode 100644 index 0000000000..8ff15d5d26 --- /dev/null +++ b/styles/src/style_tree/welcome.ts @@ -0,0 +1,157 @@ +import { with_opacity } from "../theme/color" +import { + border, + background, + foreground, + text, + TextProperties, + svg, +} from "./components" +import { interactive } from "../element" +import { useTheme } from "../theme" + +export default function welcome(): any { + const theme = useTheme() + + const checkbox_base = { + corner_radius: 4, + padding: { + left: 3, + right: 3, + top: 3, + bottom: 3, + }, + // shadow: theme.popover_shadow, + border: border(theme.highest), + margin: { + right: 8, + top: 5, + bottom: 5, + }, + } + + const interactive_text_size: TextProperties = { size: "sm" } + + return { + page_width: 320, + logo: svg( + foreground(theme.highest, "default"), + "icons/logo_96.svg", + 64, + 64 + ), + logo_subheading: { + ...text(theme.highest, "sans", "variant", { size: "md" }), + margin: { + top: 10, + bottom: 7, + }, + }, + button_group: { + margin: { + top: 8, + bottom: 16, + }, + }, + heading_group: { + margin: { + top: 8, + bottom: 12, + }, + }, + checkbox_group: { + border: border(theme.highest, "variant"), + background: with_opacity( + background(theme.highest, "hovered"), + 0.25 + ), + corner_radius: 4, + padding: { + left: 12, + top: 2, + bottom: 2, + }, + }, + button: interactive({ + base: { + background: background(theme.highest), + border: border(theme.highest, "active"), + corner_radius: 4, + margin: { + top: 4, + bottom: 4, + }, + padding: { + top: 3, + bottom: 3, + left: 7, + right: 7, + }, + ...text( + theme.highest, + "sans", + "default", + interactive_text_size + ), + }, + state: { + hovered: { + ...text( + theme.highest, + "sans", + "default", + interactive_text_size + ), + background: background(theme.highest, "hovered"), + }, + }, + }), + + usage_note: { + ...text(theme.highest, "sans", "variant", { size: "2xs" }), + padding: { + top: -4, + }, + }, + checkbox_container: { + margin: { + top: 4, + }, + padding: { + bottom: 8, + }, + }, + checkbox: { + label: { + ...text(theme.highest, "sans", interactive_text_size), + // Also supports margin, container, border, etc. + }, + icon: svg( + foreground(theme.highest, "on"), + "icons/check_12.svg", + 12, + 12 + ), + default: { + ...checkbox_base, + background: background(theme.highest, "default"), + border: border(theme.highest, "active"), + }, + checked: { + ...checkbox_base, + background: background(theme.highest, "hovered"), + border: border(theme.highest, "active"), + }, + hovered: { + ...checkbox_base, + background: background(theme.highest, "hovered"), + border: border(theme.highest, "active"), + }, + hovered_and_checked: { + ...checkbox_base, + background: background(theme.highest, "hovered"), + border: border(theme.highest, "active"), + }, + }, + } +} diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts new file mode 100644 index 0000000000..5aee3c987d --- /dev/null +++ b/styles/src/style_tree/workspace.ts @@ -0,0 +1,192 @@ +import { with_opacity } from "../theme/color" +import { + background, + border, + border_color, + foreground, + svg, + text, +} from "./components" +import statusBar from "./status_bar" +import tabBar from "./tab_bar" +import { interactive } from "../element" +import { titlebar } from "./titlebar" +import { useTheme } from "../theme" + +export default function workspace(): any { + const theme = useTheme() + + const { is_light } = theme + + return { + background: background(theme.lowest), + blank_pane: { + logo_container: { + width: 256, + height: 256, + }, + logo: svg( + with_opacity("#000000", theme.is_light ? 0.6 : 0.8), + "icons/logo_96.svg", + 256, + 256 + ), + + logo_shadow: svg( + with_opacity( + theme.is_light + ? "#FFFFFF" + : theme.lowest.base.default.background, + theme.is_light ? 1 : 0.6 + ), + "icons/logo_96.svg", + 256, + 256 + ), + keyboard_hints: { + margin: { + top: 96, + }, + corner_radius: 4, + }, + keyboard_hint: interactive({ + base: { + ...text(theme.lowest, "sans", "variant", { size: "sm" }), + padding: { + top: 3, + left: 8, + right: 8, + bottom: 3, + }, + corner_radius: 8, + }, + state: { + hovered: { + ...text(theme.lowest, "sans", "active", { size: "sm" }), + }, + }, + }), + + keyboard_hint_width: 320, + }, + joining_project_avatar: { + corner_radius: 40, + width: 80, + }, + joining_project_message: { + padding: 12, + ...text(theme.lowest, "sans", { size: "lg" }), + }, + external_location_message: { + background: background(theme.middle, "accent"), + border: border(theme.middle, "accent"), + corner_radius: 6, + padding: 12, + margin: { bottom: 8, right: 8 }, + ...text(theme.middle, "sans", "accent", { size: "xs" }), + }, + leader_border_opacity: 0.7, + leader_border_width: 2.0, + tab_bar: tabBar(), + modal: { + margin: { + bottom: 52, + top: 52, + }, + cursor: "Arrow", + }, + zoomed_background: { + cursor: "Arrow", + background: is_light + ? with_opacity(background(theme.lowest), 0.8) + : with_opacity(background(theme.highest), 0.6), + }, + zoomed_pane_foreground: { + margin: 16, + shadow: theme.modal_shadow, + border: border(theme.lowest, { overlay: true }), + }, + zoomed_panel_foreground: { + margin: 16, + border: border(theme.lowest, { overlay: true }), + }, + dock: { + left: { + border: border(theme.lowest, { right: true }), + }, + bottom: { + border: border(theme.lowest, { top: true }), + }, + right: { + border: border(theme.lowest, { left: true }), + }, + }, + pane_divider: { + color: border_color(theme.lowest), + width: 1, + }, + status_bar: statusBar(), + titlebar: titlebar(), + toolbar: { + height: 34, + background: background(theme.highest), + border: border(theme.highest, { bottom: true }), + item_spacing: 8, + nav_button: interactive({ + base: { + color: foreground(theme.highest, "on"), + icon_width: 12, + button_width: 24, + corner_radius: 6, + }, + state: { + hovered: { + color: foreground(theme.highest, "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + }, + disabled: { + color: foreground(theme.highest, "on", "disabled"), + }, + }, + }), + padding: { left: 8, right: 8, top: 4, bottom: 4 }, + }, + breadcrumb_height: 24, + breadcrumbs: interactive({ + base: { + ...text(theme.highest, "sans", "variant"), + corner_radius: 6, + padding: { + left: 6, + right: 6, + }, + }, + state: { + hovered: { + color: foreground(theme.highest, "on", "hovered"), + background: background(theme.highest, "on", "hovered"), + }, + }, + }), + disconnected_overlay: { + ...text(theme.lowest, "sans"), + background: with_opacity(background(theme.lowest), 0.8), + }, + notification: { + margin: { top: 10 }, + background: background(theme.middle), + corner_radius: 6, + padding: 12, + border: border(theme.middle), + shadow: theme.popover_shadow, + }, + notifications: { + width: 400, + margin: { right: 10, bottom: 10 }, + }, + drop_target_overlay_color: with_opacity( + foreground(theme.lowest, "variant"), + 0.5 + ), + } +} diff --git a/styles/src/system/lib/convert.ts b/styles/src/system/lib/convert.ts deleted file mode 100644 index 998f95a636..0000000000 --- a/styles/src/system/lib/convert.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** Converts a percentage scale value (0-100) to normalized scale (0-1) value. */ -export function percentageToNormalized(value: number) { - const normalized = value / 100 - return normalized -} - -/** Converts a normalized scale (0-1) value to a percentage scale (0-100) value. */ -export function normalizedToPercetage(value: number) { - const percentage = value * 100 - return percentage -} diff --git a/styles/src/system/lib/curve.ts b/styles/src/system/lib/curve.ts deleted file mode 100644 index b24f2948cf..0000000000 --- a/styles/src/system/lib/curve.ts +++ /dev/null @@ -1,26 +0,0 @@ -import bezier from "bezier-easing" -import { Curve } from "../ref/curves" - -/** - * Formats our Curve data structure into a bezier easing function. - * @param {Curve} curve - The curve to format. - * @param {Boolean} inverted - Whether or not to invert the curve. - * @returns {EasingFunction} The formatted easing function. - */ -export function curve(curve: Curve, inverted?: Boolean) { - if (inverted) { - return bezier( - curve.value[3], - curve.value[2], - curve.value[1], - curve.value[0] - ) - } - - return bezier( - curve.value[0], - curve.value[1], - curve.value[2], - curve.value[3] - ) -} diff --git a/styles/src/system/lib/generate.ts b/styles/src/system/lib/generate.ts deleted file mode 100644 index 40f7a9154c..0000000000 --- a/styles/src/system/lib/generate.ts +++ /dev/null @@ -1,159 +0,0 @@ -import bezier from "bezier-easing" -import chroma from "chroma-js" -import { Color, ColorFamily, ColorFamilyConfig, ColorScale } from "../types" -import { percentageToNormalized } from "./convert" -import { curve } from "./curve" - -// Re-export interface in a more standard format -export type EasingFunction = bezier.EasingFunction - -/** - * Generates a color, outputs it in multiple formats, and returns a variety of useful metadata. - * - * @param {EasingFunction} hueEasing - An easing function for the hue component of the color. - * @param {EasingFunction} saturationEasing - An easing function for the saturation component of the color. - * @param {EasingFunction} lightnessEasing - An easing function for the lightness component of the color. - * @param {ColorFamilyConfig} family - Configuration for the color family. - * @param {number} step - The current step. - * @param {number} steps - The total number of steps in the color scale. - * - * @returns {Color} The generated color, with its calculated contrast against black and white, as well as its LCH values, RGBA array, hexadecimal representation, and a flag indicating if it is light or dark. - */ -function generateColor( - hueEasing: EasingFunction, - saturationEasing: EasingFunction, - lightnessEasing: EasingFunction, - family: ColorFamilyConfig, - step: number, - steps: number -) { - const { hue, saturation, lightness } = family.color - - const stepHue = hueEasing(step / steps) * (hue.end - hue.start) + hue.start - const stepSaturation = - saturationEasing(step / steps) * (saturation.end - saturation.start) + - saturation.start - const stepLightness = - lightnessEasing(step / steps) * (lightness.end - lightness.start) + - lightness.start - - const color = chroma.hsl( - stepHue, - percentageToNormalized(stepSaturation), - percentageToNormalized(stepLightness) - ) - - const contrast = { - black: { - value: chroma.contrast(color, "black"), - aaPass: chroma.contrast(color, "black") >= 4.5, - aaaPass: chroma.contrast(color, "black") >= 7, - }, - white: { - value: chroma.contrast(color, "white"), - aaPass: chroma.contrast(color, "white") >= 4.5, - aaaPass: chroma.contrast(color, "white") >= 7, - }, - } - - const lch = color.lch() - const rgba = color.rgba() - const hex = color.hex() - - // 55 is a magic number. It's the lightness value at which we consider a color to be "light". - // It was picked by eye with some testing. We might want to use a more scientific approach in the future. - const isLight = lch[0] > 55 - - const result: Color = { - step, - lch, - hex, - rgba, - contrast, - isLight, - } - - return result -} - -/** - * Generates a color scale based on a color family configuration. - * - * @param {ColorFamilyConfig} config - The configuration for the color family. - * @param {Boolean} inverted - Specifies whether the color scale should be inverted or not. - * - * @returns {ColorScale} The generated color scale. - * - * @example - * ```ts - * const colorScale = generateColorScale({ - * name: "blue", - * color: { - * hue: { - * start: 210, - * end: 240, - * curve: "easeInOut" - * }, - * saturation: { - * start: 100, - * end: 100, - * curve: "easeInOut" - * }, - * lightness: { - * start: 50, - * end: 50, - * curve: "easeInOut" - * } - * } - * }); - * ``` - */ - -export function generateColorScale( - config: ColorFamilyConfig, - inverted: Boolean = false -) { - const { hue, saturation, lightness } = config.color - - // 101 steps means we get values from 0-100 - const NUM_STEPS = 101 - - const hueEasing = curve(hue.curve, inverted) - const saturationEasing = curve(saturation.curve, inverted) - const lightnessEasing = curve(lightness.curve, inverted) - - let scale: ColorScale = { - colors: [], - values: [], - } - - for (let i = 0; i < NUM_STEPS; i++) { - const color = generateColor( - hueEasing, - saturationEasing, - lightnessEasing, - config, - i, - NUM_STEPS - ) - - scale.colors.push(color) - scale.values.push(color.hex) - } - - return scale -} - -/** Generates a color family with a scale and an inverted scale. */ -export function generateColorFamily(config: ColorFamilyConfig) { - const scale = generateColorScale(config, false) - const invertedScale = generateColorScale(config, true) - - const family: ColorFamily = { - name: config.name, - scale, - invertedScale, - } - - return family -} diff --git a/styles/src/system/ref/color.ts b/styles/src/system/ref/color.ts deleted file mode 100644 index 6c0b53c35b..0000000000 --- a/styles/src/system/ref/color.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { generateColorFamily } from "../lib/generate" -import { curve } from "./curves" - -// These are the source colors for the color scales in the system. -// These should never directly be used directly in components or themes as they generate thousands of lines of code. -// Instead, use the outputs from the reference palette which exports a smaller subset of colors. - -// Token or user-facing colors should use short, clear names and a 100-900 scale to match the font weight scale. - -// Light Gray ======================================== // - -export const lightgray = generateColorFamily({ - name: "lightgray", - color: { - hue: { - start: 210, - end: 210, - curve: curve.linear, - }, - saturation: { - start: 10, - end: 15, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 50, - curve: curve.linear, - }, - }, -}) - -// Light Dark ======================================== // - -export const darkgray = generateColorFamily({ - name: "darkgray", - color: { - hue: { - start: 210, - end: 210, - curve: curve.linear, - }, - saturation: { - start: 15, - end: 20, - curve: curve.saturation, - }, - lightness: { - start: 55, - end: 8, - curve: curve.linear, - }, - }, -}) - -// Red ======================================== // - -export const red = generateColorFamily({ - name: "red", - color: { - hue: { - start: 0, - end: 0, - curve: curve.linear, - }, - saturation: { - start: 95, - end: 75, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 25, - curve: curve.lightness, - }, - }, -}) - -// Sunset ======================================== // - -export const sunset = generateColorFamily({ - name: "sunset", - color: { - hue: { - start: 15, - end: 15, - curve: curve.linear, - }, - saturation: { - start: 100, - end: 90, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 25, - curve: curve.lightness, - }, - }, -}) - -// Orange ======================================== // - -export const orange = generateColorFamily({ - name: "orange", - color: { - hue: { - start: 25, - end: 25, - curve: curve.linear, - }, - saturation: { - start: 100, - end: 95, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 20, - curve: curve.lightness, - }, - }, -}) - -// Amber ======================================== // - -export const amber = generateColorFamily({ - name: "amber", - color: { - hue: { - start: 38, - end: 38, - curve: curve.linear, - }, - saturation: { - start: 100, - end: 100, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 18, - curve: curve.lightness, - }, - }, -}) - -// Yellow ======================================== // - -export const yellow = generateColorFamily({ - name: "yellow", - color: { - hue: { - start: 48, - end: 48, - curve: curve.linear, - }, - saturation: { - start: 90, - end: 100, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 15, - curve: curve.lightness, - }, - }, -}) - -// Lemon ======================================== // - -export const lemon = generateColorFamily({ - name: "lemon", - color: { - hue: { - start: 55, - end: 55, - curve: curve.linear, - }, - saturation: { - start: 85, - end: 95, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 15, - curve: curve.lightness, - }, - }, -}) - -// Citron ======================================== // - -export const citron = generateColorFamily({ - name: "citron", - color: { - hue: { - start: 70, - end: 70, - curve: curve.linear, - }, - saturation: { - start: 85, - end: 90, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 15, - curve: curve.lightness, - }, - }, -}) - -// Lime ======================================== // - -export const lime = generateColorFamily({ - name: "lime", - color: { - hue: { - start: 85, - end: 85, - curve: curve.linear, - }, - saturation: { - start: 85, - end: 80, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 18, - curve: curve.lightness, - }, - }, -}) - -// Green ======================================== // - -export const green = generateColorFamily({ - name: "green", - color: { - hue: { - start: 108, - end: 108, - curve: curve.linear, - }, - saturation: { - start: 60, - end: 70, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 18, - curve: curve.lightness, - }, - }, -}) - -// Mint ======================================== // - -export const mint = generateColorFamily({ - name: "mint", - color: { - hue: { - start: 142, - end: 142, - curve: curve.linear, - }, - saturation: { - start: 60, - end: 75, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 20, - curve: curve.lightness, - }, - }, -}) - -// Cyan ======================================== // - -export const cyan = generateColorFamily({ - name: "cyan", - color: { - hue: { - start: 179, - end: 179, - curve: curve.linear, - }, - saturation: { - start: 70, - end: 80, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 20, - curve: curve.lightness, - }, - }, -}) - -// Sky ======================================== // - -export const sky = generateColorFamily({ - name: "sky", - color: { - hue: { - start: 195, - end: 205, - curve: curve.linear, - }, - saturation: { - start: 85, - end: 90, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 15, - curve: curve.lightness, - }, - }, -}) - -// Blue ======================================== // - -export const blue = generateColorFamily({ - name: "blue", - color: { - hue: { - start: 218, - end: 218, - curve: curve.linear, - }, - saturation: { - start: 85, - end: 70, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 15, - curve: curve.lightness, - }, - }, -}) - -// Indigo ======================================== // - -export const indigo = generateColorFamily({ - name: "indigo", - color: { - hue: { - start: 245, - end: 245, - curve: curve.linear, - }, - saturation: { - start: 60, - end: 50, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 22, - curve: curve.lightness, - }, - }, -}) - -// Purple ======================================== // - -export const purple = generateColorFamily({ - name: "purple", - color: { - hue: { - start: 260, - end: 270, - curve: curve.linear, - }, - saturation: { - start: 65, - end: 55, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 20, - curve: curve.lightness, - }, - }, -}) - -// Pink ======================================== // - -export const pink = generateColorFamily({ - name: "pink", - color: { - hue: { - start: 320, - end: 330, - curve: curve.linear, - }, - saturation: { - start: 70, - end: 65, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 32, - curve: curve.lightness, - }, - }, -}) - -// Rose ======================================== // - -export const rose = generateColorFamily({ - name: "rose", - color: { - hue: { - start: 345, - end: 345, - curve: curve.linear, - }, - saturation: { - start: 90, - end: 70, - curve: curve.saturation, - }, - lightness: { - start: 97, - end: 32, - curve: curve.lightness, - }, - }, -}) diff --git a/styles/src/system/ref/curves.ts b/styles/src/system/ref/curves.ts deleted file mode 100644 index 02002dbe9b..0000000000 --- a/styles/src/system/ref/curves.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface Curve { - name: string - value: number[] -} - -export interface Curves { - lightness: Curve - saturation: Curve - linear: Curve -} - -export const curve: Curves = { - lightness: { - name: "lightnessCurve", - value: [0.2, 0, 0.75, 1.0], - }, - saturation: { - name: "saturationCurve", - value: [0.67, 0.6, 0.55, 1.0], - }, - linear: { - name: "linear", - value: [0.5, 0.5, 0.5, 0.5], - }, -} diff --git a/styles/src/system/system.ts b/styles/src/system/system.ts deleted file mode 100644 index 619b0795c8..0000000000 --- a/styles/src/system/system.ts +++ /dev/null @@ -1,32 +0,0 @@ -import chroma from "chroma-js" -import * as colorFamily from "./ref/color" - -const color = { - lightgray: chroma - .scale(colorFamily.lightgray.scale.values) - .mode("lch") - .colors(9), - darkgray: chroma - .scale(colorFamily.darkgray.scale.values) - .mode("lch") - .colors(9), - red: chroma.scale(colorFamily.red.scale.values).mode("lch").colors(9), - sunset: chroma.scale(colorFamily.sunset.scale.values).mode("lch").colors(9), - orange: chroma.scale(colorFamily.orange.scale.values).mode("lch").colors(9), - amber: chroma.scale(colorFamily.amber.scale.values).mode("lch").colors(9), - yellow: chroma.scale(colorFamily.yellow.scale.values).mode("lch").colors(9), - lemon: chroma.scale(colorFamily.lemon.scale.values).mode("lch").colors(9), - citron: chroma.scale(colorFamily.citron.scale.values).mode("lch").colors(9), - lime: chroma.scale(colorFamily.lime.scale.values).mode("lch").colors(9), - green: chroma.scale(colorFamily.green.scale.values).mode("lch").colors(9), - mint: chroma.scale(colorFamily.mint.scale.values).mode("lch").colors(9), - cyan: chroma.scale(colorFamily.cyan.scale.values).mode("lch").colors(9), - sky: chroma.scale(colorFamily.sky.scale.values).mode("lch").colors(9), - blue: chroma.scale(colorFamily.blue.scale.values).mode("lch").colors(9), - indigo: chroma.scale(colorFamily.indigo.scale.values).mode("lch").colors(9), - purple: chroma.scale(colorFamily.purple.scale.values).mode("lch").colors(9), - pink: chroma.scale(colorFamily.pink.scale.values).mode("lch").colors(9), - rose: chroma.scale(colorFamily.rose.scale.values).mode("lch").colors(9), -} - -export { color } diff --git a/styles/src/system/types.ts b/styles/src/system/types.ts deleted file mode 100644 index 8de65a37eb..0000000000 --- a/styles/src/system/types.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Curve } from "./ref/curves" - -export interface ColorAccessibilityValue { - value: number - aaPass: boolean - aaaPass: boolean -} - -/** - * Calculates the color contrast between a specified color and its corresponding background and foreground colors. - * - * @note This implementation is currently basic – Currently we only calculate contrasts against black and white, in the future will allow for dynamic color contrast calculation based on the colors present in a given palette. - * @note The goal is to align with WCAG3 accessibility standards as they become stabilized. See the [WCAG 3 Introduction](https://www.w3.org/WAI/standards-guidelines/wcag/wcag3-intro/) for more information. - */ -export interface ColorAccessibility { - black: ColorAccessibilityValue - white: ColorAccessibilityValue -} - -export type Color = { - step: number - contrast: ColorAccessibility - hex: string - lch: number[] - rgba: number[] - isLight: boolean -} - -export interface ColorScale { - colors: Color[] - // An array of hex values for each color in the scale - values: string[] -} - -export type ColorFamily = { - name: string - scale: ColorScale - invertedScale: ColorScale -} - -export interface ColorFamilyHue { - start: number - end: number - curve: Curve -} - -export interface ColorFamilySaturation { - start: number - end: number - curve: Curve -} - -export interface ColorFamilyLightness { - start: number - end: number - curve: Curve -} - -export interface ColorFamilyConfig { - name: string - color: { - hue: ColorFamilyHue - saturation: ColorFamilySaturation - lightness: ColorFamilyLightness - } -} diff --git a/styles/src/theme/color.ts b/styles/src/theme/color.ts index 58ee4ccc7c..83c2107483 100644 --- a/styles/src/theme/color.ts +++ b/styles/src/theme/color.ts @@ -1,5 +1,5 @@ import chroma from "chroma-js" -export function withOpacity(color: string, opacity: number): string { +export function with_opacity(color: string, opacity: number): string { return chroma(color).alpha(opacity).hex() } diff --git a/styles/src/theme/colorScheme.ts b/styles/src/theme/colorScheme.ts deleted file mode 100644 index 9a81073086..0000000000 --- a/styles/src/theme/colorScheme.ts +++ /dev/null @@ -1,286 +0,0 @@ -import { Scale, Color } from "chroma-js" -import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" -export { Syntax, ThemeSyntax, SyntaxHighlightStyle } -import { - ThemeConfig, - ThemeAppearance, - ThemeConfigInputColors, -} from "./themeConfig" -import { getRamps } from "./ramps" - -export interface ColorScheme { - name: string - isLight: boolean - - lowest: Layer - middle: Layer - highest: Layer - - ramps: RampSet - - popoverShadow: Shadow - modalShadow: Shadow - - players: Players - syntax?: Partial -} - -export interface Meta { - name: string - author: string - url: string - license: License -} - -export interface License { - SPDX: SPDXExpression -} - -// License name -> License text -export interface Licenses { - [key: string]: string -} - -// FIXME: Add support for the SPDX expression syntax -export type SPDXExpression = "MIT" - -export interface Player { - cursor: string - selection: string -} - -export interface Players { - "0": Player - "1": Player - "2": Player - "3": Player - "4": Player - "5": Player - "6": Player - "7": Player -} - -export interface Shadow { - blur: number - color: string - offset: number[] -} - -export type StyleSets = keyof Layer -export interface Layer { - base: StyleSet - variant: StyleSet - on: StyleSet - accent: StyleSet - positive: StyleSet - warning: StyleSet - negative: StyleSet -} - -export interface RampSet { - neutral: Scale - red: Scale - orange: Scale - yellow: Scale - green: Scale - cyan: Scale - blue: Scale - violet: Scale - magenta: Scale -} - -export type Styles = keyof StyleSet -export interface StyleSet { - default: Style - active: Style - disabled: Style - hovered: Style - pressed: Style - inverted: Style -} - -export interface Style { - background: string - border: string - foreground: string -} - -export function createColorScheme(theme: ThemeConfig): ColorScheme { - const { - name, - appearance, - inputColor, - override: { syntax }, - } = theme - - const isLight = appearance === ThemeAppearance.Light - const colorRamps: ThemeConfigInputColors = inputColor - - // Chromajs scales from 0 to 1 flipped if isLight is true - const ramps = getRamps(isLight, colorRamps) - const lowest = lowestLayer(ramps) - const middle = middleLayer(ramps) - const highest = highestLayer(ramps) - - const popoverShadow = { - blur: 4, - color: ramps - .neutral(isLight ? 7 : 0) - .darken() - .alpha(0.2) - .hex(), // TODO used blend previously. Replace with something else - offset: [1, 2], - } - - const modalShadow = { - blur: 16, - color: ramps - .neutral(isLight ? 7 : 0) - .darken() - .alpha(0.2) - .hex(), // TODO used blend previously. Replace with something else - offset: [0, 2], - } - - const players = { - "0": player(ramps.blue), - "1": player(ramps.green), - "2": player(ramps.magenta), - "3": player(ramps.orange), - "4": player(ramps.violet), - "5": player(ramps.cyan), - "6": player(ramps.red), - "7": player(ramps.yellow), - } - - return { - name, - isLight, - - ramps, - - lowest, - middle, - highest, - - popoverShadow, - modalShadow, - - players, - syntax, - } -} - -function player(ramp: Scale): Player { - return { - selection: ramp(0.5).alpha(0.24).hex(), - cursor: ramp(0.5).hex(), - } -} - -function lowestLayer(ramps: RampSet): Layer { - return { - base: buildStyleSet(ramps.neutral, 0.2, 1), - variant: buildStyleSet(ramps.neutral, 0.2, 0.7), - on: buildStyleSet(ramps.neutral, 0.1, 1), - accent: buildStyleSet(ramps.blue, 0.1, 0.5), - positive: buildStyleSet(ramps.green, 0.1, 0.5), - warning: buildStyleSet(ramps.yellow, 0.1, 0.5), - negative: buildStyleSet(ramps.red, 0.1, 0.5), - } -} - -function middleLayer(ramps: RampSet): Layer { - return { - base: buildStyleSet(ramps.neutral, 0.1, 1), - variant: buildStyleSet(ramps.neutral, 0.1, 0.7), - on: buildStyleSet(ramps.neutral, 0, 1), - accent: buildStyleSet(ramps.blue, 0.1, 0.5), - positive: buildStyleSet(ramps.green, 0.1, 0.5), - warning: buildStyleSet(ramps.yellow, 0.1, 0.5), - negative: buildStyleSet(ramps.red, 0.1, 0.5), - } -} - -function highestLayer(ramps: RampSet): Layer { - return { - base: buildStyleSet(ramps.neutral, 0, 1), - variant: buildStyleSet(ramps.neutral, 0, 0.7), - on: buildStyleSet(ramps.neutral, 0.1, 1), - accent: buildStyleSet(ramps.blue, 0.1, 0.5), - positive: buildStyleSet(ramps.green, 0.1, 0.5), - warning: buildStyleSet(ramps.yellow, 0.1, 0.5), - negative: buildStyleSet(ramps.red, 0.1, 0.5), - } -} - -function buildStyleSet( - ramp: Scale, - backgroundBase: number, - foregroundBase: number, - step: number = 0.08 -): StyleSet { - let styleDefinitions = buildStyleDefinition( - backgroundBase, - foregroundBase, - step - ) - - function colorString(indexOrColor: number | Color): string { - if (typeof indexOrColor === "number") { - return ramp(indexOrColor).hex() - } else { - return indexOrColor.hex() - } - } - - function buildStyle(style: Styles): Style { - return { - background: colorString(styleDefinitions.background[style]), - border: colorString(styleDefinitions.border[style]), - foreground: colorString(styleDefinitions.foreground[style]), - } - } - - return { - default: buildStyle("default"), - hovered: buildStyle("hovered"), - pressed: buildStyle("pressed"), - active: buildStyle("active"), - disabled: buildStyle("disabled"), - inverted: buildStyle("inverted"), - } -} - -function buildStyleDefinition( - bgBase: number, - fgBase: number, - step: number = 0.08 -) { - return { - background: { - default: bgBase, - hovered: bgBase + step, - pressed: bgBase + step * 1.5, - active: bgBase + step * 2.2, - disabled: bgBase, - inverted: fgBase + step * 6, - }, - border: { - default: bgBase + step * 1, - hovered: bgBase + step, - pressed: bgBase + step, - active: bgBase + step * 3, - disabled: bgBase + step * 0.5, - inverted: bgBase - step * 3, - }, - foreground: { - default: fgBase, - hovered: fgBase, - pressed: fgBase, - active: fgBase + step * 6, - disabled: bgBase + step * 4, - inverted: bgBase + step * 2, - }, - } -} diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts new file mode 100644 index 0000000000..dff4c3dbc4 --- /dev/null +++ b/styles/src/theme/create_theme.ts @@ -0,0 +1,282 @@ +import { Scale, Color } from "chroma-js" +import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" +export { Syntax, ThemeSyntax, SyntaxHighlightStyle } +import { + ThemeConfig, + ThemeAppearance, + ThemeConfigInputColors, +} from "./theme_config" +import { get_ramps } from "./ramps" + +export interface Theme { + name: string + is_light: boolean + + lowest: Layer + middle: Layer + highest: Layer + + ramps: RampSet + + popover_shadow: Shadow + modal_shadow: Shadow + + players: Players + syntax?: Partial +} + +export interface Meta { + name: string + author: string + url: string + license: License +} + +export interface License { + SPDX: SPDXExpression +} + +// License name -> License text +export interface Licenses { + [key: string]: string +} + +// FIXME: Add support for the SPDX expression syntax +export type SPDXExpression = "MIT" + +export interface Player { + cursor: string + selection: string +} + +export interface Players { + "0": Player + "1": Player + "2": Player + "3": Player + "4": Player + "5": Player + "6": Player + "7": Player +} + +export interface Shadow { + blur: number + color: string + offset: number[] +} + +export type StyleSets = keyof Layer +export interface Layer { + base: StyleSet + variant: StyleSet + on: StyleSet + accent: StyleSet + positive: StyleSet + warning: StyleSet + negative: StyleSet +} + +export interface RampSet { + neutral: Scale + red: Scale + orange: Scale + yellow: Scale + green: Scale + cyan: Scale + blue: Scale + violet: Scale + magenta: Scale +} + +export type Styles = keyof StyleSet +export interface StyleSet { + default: Style + active: Style + disabled: Style + hovered: Style + pressed: Style + inverted: Style +} + +export interface Style { + background: string + border: string + foreground: string +} + +export function create_theme(theme: ThemeConfig): Theme { + const { + name, + appearance, + input_color, + override: { syntax }, + } = theme + + const is_light = appearance === ThemeAppearance.Light + const color_ramps: ThemeConfigInputColors = input_color + + // Chromajs scales from 0 to 1 flipped if is_light is true + const ramps = get_ramps(is_light, color_ramps) + const lowest = lowest_layer(ramps) + const middle = middle_layer(ramps) + const highest = highest_layer(ramps) + + const popover_shadow = { + blur: 4, + color: ramps + .neutral(is_light ? 7 : 0) + .darken() + .alpha(0.2) + .hex(), // TODO used blend previously. Replace with something else + offset: [1, 2], + } + + const modal_shadow = { + blur: 16, + color: ramps + .neutral(is_light ? 7 : 0) + .darken() + .alpha(0.2) + .hex(), // TODO used blend previously. Replace with something else + offset: [0, 2], + } + + const players = { + "0": player(ramps.blue), + "1": player(ramps.green), + "2": player(ramps.magenta), + "3": player(ramps.orange), + "4": player(ramps.violet), + "5": player(ramps.cyan), + "6": player(ramps.red), + "7": player(ramps.yellow), + } + + return { + name, + is_light, + + ramps, + + lowest, + middle, + highest, + + popover_shadow, + modal_shadow, + + players, + syntax, + } +} + +function player(ramp: Scale): Player { + return { + selection: ramp(0.5).alpha(0.24).hex(), + cursor: ramp(0.5).hex(), + } +} + +function lowest_layer(ramps: RampSet): Layer { + return { + base: build_style_set(ramps.neutral, 0.2, 1), + variant: build_style_set(ramps.neutral, 0.2, 0.7), + on: build_style_set(ramps.neutral, 0.1, 1), + accent: build_style_set(ramps.blue, 0.1, 0.5), + positive: build_style_set(ramps.green, 0.1, 0.5), + warning: build_style_set(ramps.yellow, 0.1, 0.5), + negative: build_style_set(ramps.red, 0.1, 0.5), + } +} + +function middle_layer(ramps: RampSet): Layer { + return { + base: build_style_set(ramps.neutral, 0.1, 1), + variant: build_style_set(ramps.neutral, 0.1, 0.7), + on: build_style_set(ramps.neutral, 0, 1), + accent: build_style_set(ramps.blue, 0.1, 0.5), + positive: build_style_set(ramps.green, 0.1, 0.5), + warning: build_style_set(ramps.yellow, 0.1, 0.5), + negative: build_style_set(ramps.red, 0.1, 0.5), + } +} + +function highest_layer(ramps: RampSet): Layer { + return { + base: build_style_set(ramps.neutral, 0, 1), + variant: build_style_set(ramps.neutral, 0, 0.7), + on: build_style_set(ramps.neutral, 0.1, 1), + accent: build_style_set(ramps.blue, 0.1, 0.5), + positive: build_style_set(ramps.green, 0.1, 0.5), + warning: build_style_set(ramps.yellow, 0.1, 0.5), + negative: build_style_set(ramps.red, 0.1, 0.5), + } +} + +function build_style_set( + ramp: Scale, + background_base: number, + foreground_base: number, + step = 0.08 +): StyleSet { + const style_definitions = build_style_definition( + background_base, + foreground_base, + step + ) + + function color_string(index_or_color: number | Color): string { + if (typeof index_or_color === "number") { + return ramp(index_or_color).hex() + } else { + return index_or_color.hex() + } + } + + function build_style(style: Styles): Style { + return { + background: color_string(style_definitions.background[style]), + border: color_string(style_definitions.border[style]), + foreground: color_string(style_definitions.foreground[style]), + } + } + + return { + default: build_style("default"), + hovered: build_style("hovered"), + pressed: build_style("pressed"), + active: build_style("active"), + disabled: build_style("disabled"), + inverted: build_style("inverted"), + } +} + +function build_style_definition(bg_base: number, fg_base: number, step = 0.08) { + return { + background: { + default: bg_base, + hovered: bg_base + step, + pressed: bg_base + step * 1.5, + active: bg_base + step * 2.2, + disabled: bg_base, + inverted: fg_base + step * 6, + }, + border: { + default: bg_base + step * 1, + hovered: bg_base + step, + pressed: bg_base + step, + active: bg_base + step * 3, + disabled: bg_base + step * 0.5, + inverted: bg_base - step * 3, + }, + foreground: { + default: fg_base, + hovered: fg_base, + pressed: fg_base, + active: fg_base + step * 6, + disabled: bg_base + step * 4, + inverted: bg_base + step * 2, + }, + } +} diff --git a/styles/src/theme/index.ts b/styles/src/theme/index.ts index 2bf625521c..ca8aaa461f 100644 --- a/styles/src/theme/index.ts +++ b/styles/src/theme/index.ts @@ -1,4 +1,25 @@ -export * from "./colorScheme" +import { create } from "zustand" +import { Theme } from "./create_theme" + +type ThemeState = { + theme: Theme | undefined + setTheme: (theme: Theme) => void +} + +export const useThemeStore = create((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 "./themeConfig" +export * from "./theme_config" diff --git a/styles/src/theme/ramps.ts b/styles/src/theme/ramps.ts index f8c44ba3f9..c5b915a8c5 100644 --- a/styles/src/theme/ramps.ts +++ b/styles/src/theme/ramps.ts @@ -1,14 +1,14 @@ import chroma, { Color, Scale } from "chroma-js" -import { RampSet } from "./colorScheme" +import { RampSet } from "./create_theme" import { ThemeConfigInputColors, ThemeConfigInputColorsKeys, -} from "./themeConfig" +} from "./theme_config" -export function colorRamp(color: Color): Scale { - let endColor = color.desaturate(1).brighten(5) - let startColor = color.desaturate(1).darken(4) - return chroma.scale([startColor, color, endColor]).mode("lab") +export function color_ramp(color: Color): Scale { + const end_color = color.desaturate(1).brighten(5) + const start_color = color.desaturate(1).darken(4) + return chroma.scale([start_color, color, end_color]).mode("lab") } /** @@ -18,29 +18,29 @@ export function colorRamp(color: Color): Scale { theme so that we don't modify the passed in ramps. This combined with an error in the type definitions for chroma js means we have to cast the colors function to any in order to get the colors back out from the original ramps. - * @param isLight - * @param colorRamps - * @returns + * @param is_light + * @param color_ramps + * @returns */ -export function getRamps( - isLight: boolean, - colorRamps: ThemeConfigInputColors +export function get_ramps( + is_light: boolean, + color_ramps: ThemeConfigInputColors ): RampSet { - const ramps: RampSet = {} as any - const colorsKeys = Object.keys(colorRamps) as ThemeConfigInputColorsKeys[] + const ramps: RampSet = {} as any // eslint-disable-line @typescript-eslint/no-explicit-any + const color_keys = Object.keys(color_ramps) as ThemeConfigInputColorsKeys[] - if (isLight) { - for (const rampName of colorsKeys) { - ramps[rampName] = chroma.scale( - colorRamps[rampName].colors(100).reverse() + if (is_light) { + for (const ramp_name of color_keys) { + ramps[ramp_name] = chroma.scale( + color_ramps[ramp_name].colors(100).reverse() ) } - ramps.neutral = chroma.scale(colorRamps.neutral.colors(100).reverse()) + ramps.neutral = chroma.scale(color_ramps.neutral.colors(100).reverse()) } else { - for (const rampName of colorsKeys) { - ramps[rampName] = chroma.scale(colorRamps[rampName].colors(100)) + for (const ramp_name of color_keys) { + ramps[ramp_name] = chroma.scale(color_ramps[ramp_name].colors(100)) } - ramps.neutral = chroma.scale(colorRamps.neutral.colors(100)) + ramps.neutral = chroma.scale(color_ramps.neutral.colors(100)) } return ramps diff --git a/styles/src/theme/syntax.ts b/styles/src/theme/syntax.ts index 380fd31786..540a1d0ff9 100644 --- a/styles/src/theme/syntax.ts +++ b/styles/src/theme/syntax.ts @@ -1,6 +1,5 @@ import deepmerge from "deepmerge" -import { FontWeight, fontWeights } from "../common" -import { ColorScheme } from "./colorScheme" +import { FontWeight, font_weights, useTheme } from "../common" import chroma from "chroma-js" export interface SyntaxHighlightStyle { @@ -17,13 +16,14 @@ export interface Syntax { "comment.doc": SyntaxHighlightStyle primary: SyntaxHighlightStyle predictive: SyntaxHighlightStyle + hint: SyntaxHighlightStyle // === Formatted Text ====== / emphasis: SyntaxHighlightStyle "emphasis.strong": SyntaxHighlightStyle title: SyntaxHighlightStyle - linkUri: SyntaxHighlightStyle - linkText: SyntaxHighlightStyle + link_uri: SyntaxHighlightStyle + link_text: SyntaxHighlightStyle /** md: indented_code_block, fenced_code_block, code_span */ "text.literal": SyntaxHighlightStyle @@ -56,7 +56,7 @@ export interface Syntax { // == Types ====== / // We allow Function here because all JS objects literals have this property - constructor: SyntaxHighlightStyle | Function + constructor: SyntaxHighlightStyle | Function // eslint-disable-line @typescript-eslint/ban-types variant: SyntaxHighlightStyle type: SyntaxHighlightStyle // js: predefined_type @@ -116,25 +116,25 @@ export interface Syntax { export type ThemeSyntax = Partial -const defaultSyntaxHighlightStyle: Omit = { +const default_syntax_highlight_style: Omit = { weight: "normal", underline: false, italic: false, } -function buildDefaultSyntax(colorScheme: 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: { [key: string]: Omit } = {} - const light = colorScheme.isLight - // then spread the default to each style for (const key of Object.keys({} as Syntax)) { syntax[key as keyof Syntax] = { - ...defaultSyntaxHighlightStyle, + ...default_syntax_highlight_style, } } @@ -142,35 +142,46 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { // predictive color distinct from any other color in the theme const predictive = chroma .mix( - colorScheme.ramps.neutral(0.4).hex(), - colorScheme.ramps.blue(0.4).hex(), + theme.ramps.neutral(0.4).hex(), + theme.ramps.blue(0.4).hex(), + 0.45, + "lch" + ) + .hex() + // Mix the neutral and green colors to get a + // hint color distinct from any other color in the theme + const hint = chroma + .mix( + theme.ramps.neutral(0.6).hex(), + theme.ramps.blue(0.4).hex(), 0.45, "lch" ) .hex() const color = { - primary: colorScheme.ramps.neutral(1).hex(), - comment: colorScheme.ramps.neutral(0.71).hex(), - punctuation: colorScheme.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, - emphasis: colorScheme.ramps.blue(0.5).hex(), - string: colorScheme.ramps.orange(0.5).hex(), - function: colorScheme.ramps.yellow(0.5).hex(), - type: colorScheme.ramps.cyan(0.5).hex(), - constructor: colorScheme.ramps.blue(0.5).hex(), - variant: colorScheme.ramps.blue(0.5).hex(), - property: colorScheme.ramps.blue(0.5).hex(), - enum: colorScheme.ramps.orange(0.5).hex(), - operator: colorScheme.ramps.orange(0.5).hex(), - number: colorScheme.ramps.green(0.5).hex(), - boolean: colorScheme.ramps.green(0.5).hex(), - constant: colorScheme.ramps.green(0.5).hex(), - keyword: colorScheme.ramps.blue(0.5).hex(), + hint: hint, + 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 - const defaultSyntax: Syntax = { + const default_syntax: Syntax = { ...syntax, comment: { color: color.comment, @@ -185,23 +196,27 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { color: color.predictive, italic: true, }, + hint: { + color: color.hint, + weight: font_weights.bold, + }, emphasis: { color: color.emphasis, }, "emphasis.strong": { color: color.emphasis, - weight: fontWeights.bold, + weight: font_weights.bold, }, title: { color: color.primary, - weight: fontWeights.bold, + weight: font_weights.bold, }, - linkUri: { - color: colorScheme.ramps.green(0.5).hex(), + link_uri: { + color: theme.ramps.green(0.5).hex(), underline: true, }, - linkText: { - color: colorScheme.ramps.orange(0.5).hex(), + link_text: { + color: theme.ramps.orange(0.5).hex(), italic: true, }, "text.literal": { @@ -217,7 +232,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { color: color.punctuation, }, "punctuation.special": { - color: colorScheme.ramps.neutral(0.86).hex(), + color: theme.ramps.neutral(0.86).hex(), }, "punctuation.list_marker": { color: color.punctuation, @@ -238,10 +253,10 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { color: color.string, }, constructor: { - color: colorScheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, variant: { - color: colorScheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, type: { color: color.type, @@ -250,16 +265,16 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { color: color.primary, }, label: { - color: colorScheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, tag: { - color: colorScheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, attribute: { - color: colorScheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, property: { - color: colorScheme.ramps.blue(0.5).hex(), + color: theme.ramps.blue(0.5).hex(), }, constant: { color: color.constant, @@ -290,17 +305,21 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax { }, } - return defaultSyntax + return default_syntax } -function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax { - if (!colorScheme.syntax) { - return defaultSyntax +export function build_syntax(): Syntax { + const theme = useTheme() + + const default_syntax: Syntax = build_default_syntax() + + if (!theme.syntax) { + return default_syntax } - return deepmerge>( - defaultSyntax, - colorScheme.syntax, + const syntax = deepmerge>( + default_syntax, + theme.syntax, { arrayMerge: (destinationArray, sourceArray) => [ ...destinationArray, @@ -308,12 +327,6 @@ function mergeSyntax(defaultSyntax: Syntax, colorScheme: ColorScheme): Syntax { ], } ) -} - -export function buildSyntax(colorScheme: ColorScheme): Syntax { - const defaultSyntax: Syntax = buildDefaultSyntax(colorScheme) - - const syntax = mergeSyntax(defaultSyntax, colorScheme) return syntax } diff --git a/styles/src/theme/themeConfig.ts b/styles/src/theme/themeConfig.ts deleted file mode 100644 index 176ae83bb7..0000000000 --- a/styles/src/theme/themeConfig.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Scale, Color } from "chroma-js" -import { Syntax } from "./syntax" - -interface ThemeMeta { - /** The name of the theme */ - name: string - /** The theme's appearance. Either `light` or `dark`. */ - appearance: ThemeAppearance - /** The author of the theme - * - * Ideally formatted as `Full Name ` - * - * Example: `John Doe ` - */ - author: string - /** SPDX License string - * - * Example: `MIT` - */ - licenseType?: string | ThemeLicenseType - licenseUrl?: string - licenseFile: string - themeUrl?: string -} - -export type ThemeFamilyMeta = Pick< - ThemeMeta, - "name" | "author" | "licenseType" | "licenseUrl" -> - -export interface ThemeConfigInputColors { - neutral: Scale - red: Scale - orange: Scale - yellow: Scale - green: Scale - cyan: Scale - blue: Scale - violet: Scale - magenta: Scale -} - -export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors - -/** Allow any part of a syntax highlight style to be overriden by the theme - * - * Example: - * ```ts - * override: { - * syntax: { - * boolean: { - * underline: true, - * }, - * }, - * } - * ``` - */ -export type ThemeConfigInputSyntax = Partial - -interface ThemeConfigOverrides { - syntax: ThemeConfigInputSyntax -} - -type ThemeConfigProperties = ThemeMeta & { - inputColor: ThemeConfigInputColors - 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 - -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", -} - -export enum ThemeLicenseType { - MIT = "MIT", - Apache2 = "Apache License 2.0", -} - -export type ThemeFamilyItem = - | ThemeConfig - | { light: ThemeConfig; dark: ThemeConfig } - -type ThemeFamilyProperties = Partial> & { - 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 diff --git a/styles/src/theme/theme_config.ts b/styles/src/theme/theme_config.ts new file mode 100644 index 0000000000..bc8f07425f --- /dev/null +++ b/styles/src/theme/theme_config.ts @@ -0,0 +1,81 @@ +import { Scale, Color } from "chroma-js" +import { Syntax } from "./syntax" + +interface ThemeMeta { + /** The name of the theme */ + name: string + /** The theme's appearance. Either `light` or `dark`. */ + appearance: ThemeAppearance + /** The author of the theme + * + * Ideally formatted as `Full Name ` + * + * Example: `John Doe ` + */ + author: string + /** SPDX License string + * + * Example: `MIT` + */ + license_type?: string | ThemeLicenseType + license_url?: string + license_file: string + theme_url?: string +} + +export type ThemeFamilyMeta = Pick< + ThemeMeta, + "name" | "author" | "license_type" | "license_url" +> + +export interface ThemeConfigInputColors { + neutral: Scale + red: Scale + orange: Scale + yellow: Scale + green: Scale + cyan: Scale + blue: Scale + violet: Scale + magenta: Scale +} + +export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors + +/** Allow any part of a syntax highlight style to be overriden by the theme + * + * Example: + * ```ts + * override: { + * syntax: { + * boolean: { + * underline: true, + * }, + * }, + * } + * ``` + */ +export type ThemeConfigInputSyntax = Partial + +interface ThemeConfigOverrides { + syntax: ThemeConfigInputSyntax +} + +type ThemeConfigProperties = ThemeMeta & { + input_color: ThemeConfigInputColors + override: ThemeConfigOverrides +} + +export type ThemeConfig = { + [K in keyof ThemeConfigProperties]: ThemeConfigProperties[K] +} + +export enum ThemeAppearance { + Light = "light", + Dark = "dark", +} + +export enum ThemeLicenseType { + MIT = "MIT", + Apache2 = "Apache License 2.0", +} diff --git a/styles/src/theme/tokens/colorScheme.ts b/styles/src/theme/tokens/colorScheme.ts deleted file mode 100644 index 84b8db5ce0..0000000000 --- a/styles/src/theme/tokens/colorScheme.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { SingleBoxShadowToken, SingleColorToken, SingleOtherToken, TokenTypes } from "@tokens-studio/types" -import { ColorScheme, Shadow, SyntaxHighlightStyle, ThemeSyntax } from "../colorScheme" -import { LayerToken, layerToken } from "./layer" -import { PlayersToken, playersToken } from "./players" -import { colorToken } from "./token" -import { Syntax } from "../syntax"; -import editor from "../../styleTree/editor" - -interface ColorSchemeTokens { - name: SingleOtherToken - appearance: SingleOtherToken - lowest: LayerToken - middle: LayerToken - highest: LayerToken - players: PlayersToken - popoverShadow: SingleBoxShadowToken - modalShadow: SingleBoxShadowToken - syntax?: Partial -} - -const createShadowToken = (shadow: Shadow, tokenName: string): SingleBoxShadowToken => { - return { - name: tokenName, - type: TokenTypes.BOX_SHADOW, - value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}` - }; -}; - -const popoverShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => { - const shadow = colorScheme.popoverShadow; - return createShadowToken(shadow, "popoverShadow"); -}; - -const modalShadowToken = (colorScheme: ColorScheme): SingleBoxShadowToken => { - const shadow = colorScheme.modalShadow; - return createShadowToken(shadow, "modalShadow"); -}; - -type ThemeSyntaxColorTokens = Record - -function syntaxHighlightStyleColorTokens(syntax: Syntax): ThemeSyntaxColorTokens { - const styleKeys = Object.keys(syntax) as (keyof Syntax)[] - - return styleKeys.reduce((acc, styleKey) => { - // Hack: The type of a style could be "Function" - // This can happen because we have a "constructor" property on the syntax object - // and a "constructor" property on the prototype of the syntax object - // To work around this just assert that the type of the style is not a function - if (!syntax[styleKey] || typeof syntax[styleKey] === 'function') return acc; - const { color } = syntax[styleKey] as Required; - return { ...acc, [styleKey]: colorToken(styleKey, color) }; - }, {} as ThemeSyntaxColorTokens); -} - -const syntaxTokens = (colorScheme: ColorScheme): ColorSchemeTokens['syntax'] => { - const syntax = editor(colorScheme).syntax - - return syntaxHighlightStyleColorTokens(syntax) -} - -export function colorSchemeTokens(colorScheme: ColorScheme): ColorSchemeTokens { - return { - name: { - name: "themeName", - value: colorScheme.name, - type: TokenTypes.OTHER, - }, - appearance: { - name: "themeAppearance", - value: colorScheme.isLight ? "light" : "dark", - type: TokenTypes.OTHER, - }, - lowest: layerToken(colorScheme.lowest, "lowest"), - middle: layerToken(colorScheme.middle, "middle"), - highest: layerToken(colorScheme.highest, "highest"), - popoverShadow: popoverShadowToken(colorScheme), - modalShadow: modalShadowToken(colorScheme), - players: playersToken(colorScheme), - syntax: syntaxTokens(colorScheme), - } -} diff --git a/styles/src/theme/tokens/layer.ts b/styles/src/theme/tokens/layer.ts index 3ee813b8c4..6b4e1d79f7 100644 --- a/styles/src/theme/tokens/layer.ts +++ b/styles/src/theme/tokens/layer.ts @@ -1,11 +1,11 @@ -import { SingleColorToken } from "@tokens-studio/types"; -import { Layer, Style, StyleSet } from "../colorScheme"; -import { colorToken } from "./token"; +import { SingleColorToken } from "@tokens-studio/types" +import { Layer, Style, StyleSet } from "../create_theme" +import { color_token } from "./token" interface StyleToken { - background: SingleColorToken, - border: SingleColorToken, - foreground: SingleColorToken, + background: SingleColorToken + border: SingleColorToken + foreground: SingleColorToken } interface StyleSetToken { @@ -27,34 +27,37 @@ export interface LayerToken { negative: StyleSetToken } -export const styleToken = (style: Style, name: string): StyleToken => { +export const style_token = (style: Style, name: string): StyleToken => { const token = { - background: colorToken(`${name}Background`, style.background), - border: colorToken(`${name}Border`, style.border), - foreground: colorToken(`${name}Foreground`, style.foreground), + background: color_token(`${name}Background`, style.background), + border: color_token(`${name}Border`, style.border), + foreground: color_token(`${name}Foreground`, style.foreground), } return token } -export const styleSetToken = (styleSet: StyleSet, name: string): StyleSetToken => { - const token: StyleSetToken = {} as StyleSetToken; +export const style_set_token = ( + style_set: StyleSet, + name: string +): StyleSetToken => { + const token: StyleSetToken = {} as StyleSetToken - for (const style in styleSet) { - const s = style as keyof StyleSet; - token[s] = styleToken(styleSet[s], `${name}${style}`); + for (const style in style_set) { + const s = style as keyof StyleSet + token[s] = style_token(style_set[s], `${name}${style}`) } - return token; + return token } -export const layerToken = (layer: Layer, name: string): LayerToken => { - const token: LayerToken = {} as LayerToken; +export const layer_token = (layer: Layer, name: string): LayerToken => { + const token: LayerToken = {} as LayerToken - for (const styleSet in layer) { - const s = styleSet as keyof Layer; - token[s] = styleSetToken(layer[s], `${name}${styleSet}`); + for (const style_set in layer) { + const s = style_set as keyof Layer + token[s] = style_set_token(layer[s], `${name}${style_set}`) } - return token; + return token } diff --git a/styles/src/theme/tokens/players.ts b/styles/src/theme/tokens/players.ts index b5f5538b2e..4bf605aa93 100644 --- a/styles/src/theme/tokens/players.ts +++ b/styles/src/theme/tokens/players.ts @@ -1,28 +1,37 @@ import { SingleColorToken } from "@tokens-studio/types" -import { ColorScheme, Players } from "../../common" -import { colorToken } from "./token" +import { color_token } from "./token" +import { Players } from "../create_theme" +import { useTheme } from "../../../src/common" export type PlayerToken = Record<"selection" | "cursor", SingleColorToken> export type PlayersToken = Record -function buildPlayerToken(colorScheme: ColorScheme, index: number): PlayerToken { - - const playerNumber = index.toString() as keyof Players +function build_player_token(index: number): PlayerToken { + const theme = useTheme() + const player_number = index.toString() as keyof Players return { - selection: colorToken(`player${index}Selection`, colorScheme.players[playerNumber].selection), - cursor: colorToken(`player${index}Cursor`, colorScheme.players[playerNumber].cursor), + selection: color_token( + `player${index}Selection`, + theme.players[player_number].selection + ), + cursor: color_token( + `player${index}Cursor`, + theme.players[player_number].cursor + ), } } -export const playersToken = (colorScheme: ColorScheme): PlayersToken => ({ - "0": buildPlayerToken(colorScheme, 0), - "1": buildPlayerToken(colorScheme, 1), - "2": buildPlayerToken(colorScheme, 2), - "3": buildPlayerToken(colorScheme, 3), - "4": buildPlayerToken(colorScheme, 4), - "5": buildPlayerToken(colorScheme, 5), - "6": buildPlayerToken(colorScheme, 6), - "7": buildPlayerToken(colorScheme, 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), + } +} diff --git a/styles/src/theme/tokens/theme.ts b/styles/src/theme/tokens/theme.ts new file mode 100644 index 0000000000..f759bc8139 --- /dev/null +++ b/styles/src/theme/tokens/theme.ts @@ -0,0 +1,101 @@ +import { + SingleBoxShadowToken, + SingleColorToken, + SingleOtherToken, + TokenTypes, +} from "@tokens-studio/types" +import { + Shadow, + SyntaxHighlightStyle, + ThemeSyntax, +} 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 ThemeTokens { + name: SingleOtherToken + appearance: SingleOtherToken + lowest: LayerToken + middle: LayerToken + highest: LayerToken + players: PlayersToken + popover_shadow: SingleBoxShadowToken + modal_shadow: SingleBoxShadowToken + syntax?: Partial +} + +const create_shadow_token = ( + shadow: Shadow, + token_name: string +): SingleBoxShadowToken => { + return { + name: token_name, + type: TokenTypes.BOX_SHADOW, + value: `${shadow.offset[0]}px ${shadow.offset[1]}px ${shadow.blur}px 0px ${shadow.color}`, + } +} + +const popover_shadow_token = (): SingleBoxShadowToken => { + const theme = useTheme() + const shadow = theme.popover_shadow + return create_shadow_token(shadow, "popover_shadow") +} + +const modal_shadow_token = (): SingleBoxShadowToken => { + const theme = useTheme() + const shadow = theme.modal_shadow + return create_shadow_token(shadow, "modal_shadow") +} + +type ThemeSyntaxColorTokens = Record + +function syntax_highlight_style_color_tokens( + syntax: Syntax +): ThemeSyntaxColorTokens { + const style_keys = Object.keys(syntax) as (keyof Syntax)[] + + return style_keys.reduce((acc, style_key) => { + // Hack: The type of a style could be "Function" + // This can happen because we have a "constructor" property on the syntax object + // and a "constructor" property on the prototype of the syntax object + // To work around this just assert that the type of the style is not a function + if (!syntax[style_key] || typeof syntax[style_key] === "function") + return acc + const { color } = syntax[style_key] as Required + return { ...acc, [style_key]: color_token(style_key, color) } + }, {} as ThemeSyntaxColorTokens) +} + +const syntax_tokens = (): ThemeTokens["syntax"] => { + const syntax = editor().syntax + + return syntax_highlight_style_color_tokens(syntax) +} + +export function theme_tokens(): ThemeTokens { + const theme = useTheme() + + return { + name: { + name: "themeName", + value: theme.name, + type: TokenTypes.OTHER, + }, + appearance: { + name: "themeAppearance", + value: theme.is_light ? "light" : "dark", + type: TokenTypes.OTHER, + }, + lowest: layer_token(theme.lowest, "lowest"), + middle: layer_token(theme.middle, "middle"), + highest: layer_token(theme.highest, "highest"), + popover_shadow: popover_shadow_token(), + modal_shadow: modal_shadow_token(), + players: players_token(), + syntax: syntax_tokens(), + } +} diff --git a/styles/src/theme/tokens/token.ts b/styles/src/theme/tokens/token.ts index 3e5187dd64..60e846ce94 100644 --- a/styles/src/theme/tokens/token.ts +++ b/styles/src/theme/tokens/token.ts @@ -1,6 +1,10 @@ import { SingleColorToken, TokenTypes } from "@tokens-studio/types" -export function colorToken(name: string, value: string, description?: string): SingleColorToken { +export function color_token( + name: string, + value: string, + description?: string +): SingleColorToken { const token: SingleColorToken = { name, type: TokenTypes.COLOR, @@ -8,7 +12,8 @@ export function colorToken(name: string, value: string, description?: string): S description, } - if (!token.value || token.value === '') throw new Error("Color token must have a value") + if (!token.value || token.value === "") + throw new Error("Color token must have a value") return token } diff --git a/styles/src/themes/andromeda/LICENSE b/styles/src/themes/andromeda/LICENSE index bdd549491f..9422adafa4 100644 --- a/styles/src/themes/andromeda/LICENSE +++ b/styles/src/themes/andromeda/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/andromeda/andromeda.ts b/styles/src/themes/andromeda/andromeda.ts index 52c29bb2ec..18699d21cd 100644 --- a/styles/src/themes/andromeda/andromeda.ts +++ b/styles/src/themes/andromeda/andromeda.ts @@ -1,6 +1,6 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, @@ -10,10 +10,10 @@ export const dark: ThemeConfig = { name: "Andromeda", author: "EliverLara", appearance: ThemeAppearance.Dark, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/EliverLara/Andromeda", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/EliverLara/Andromeda", + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma .scale([ "#1E2025", @@ -26,14 +26,14 @@ export const dark: ThemeConfig = { "#F7F7F8", ]) .domain([0, 0.15, 0.25, 0.35, 0.7, 0.8, 0.9, 1]), - red: colorRamp(chroma("#F92672")), - orange: colorRamp(chroma("#F39C12")), - yellow: colorRamp(chroma("#FFE66D")), - green: colorRamp(chroma("#96E072")), - cyan: colorRamp(chroma("#00E8C6")), - blue: colorRamp(chroma("#0CA793")), - violet: colorRamp(chroma("#8A3FA6")), - magenta: colorRamp(chroma("#C74DED")), + red: color_ramp(chroma("#F92672")), + orange: color_ramp(chroma("#F39C12")), + yellow: color_ramp(chroma("#FFE66D")), + green: color_ramp(chroma("#96E072")), + cyan: color_ramp(chroma("#00E8C6")), + blue: color_ramp(chroma("#0CA793")), + violet: color_ramp(chroma("#8A3FA6")), + magenta: color_ramp(chroma("#C74DED")), }, override: { syntax: {} }, } diff --git a/styles/src/themes/atelier/LICENSE b/styles/src/themes/atelier/LICENSE index 9f92967a04..47c46d0429 100644 --- a/styles/src/themes/atelier/LICENSE +++ b/styles/src/themes/atelier/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/atelier/atelier-cave-dark.ts b/styles/src/themes/atelier/atelier-cave-dark.ts index ebec67b4c2..faf957b642 100644 --- a/styles/src/themes/atelier/atelier-cave-dark.ts +++ b/styles/src/themes/atelier/atelier-cave-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Cave Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-cave-light.ts b/styles/src/themes/atelier/atelier-cave-light.ts index c1b7a05d47..856cd30043 100644 --- a/styles/src/themes/atelier/atelier-cave-light.ts +++ b/styles/src/themes/atelier/atelier-cave-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Cave Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-dune-dark.ts b/styles/src/themes/atelier/atelier-dune-dark.ts index c2ebc424e7..fb67fd2471 100644 --- a/styles/src/themes/atelier/atelier-dune-dark.ts +++ b/styles/src/themes/atelier/atelier-dune-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Dune Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-dune-light.ts b/styles/src/themes/atelier/atelier-dune-light.ts index 01cb1d67cb..5e9e5b6927 100644 --- a/styles/src/themes/atelier/atelier-dune-light.ts +++ b/styles/src/themes/atelier/atelier-dune-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Dune Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-estuary-dark.ts b/styles/src/themes/atelier/atelier-estuary-dark.ts index 8e32c1f68f..0badf4371e 100644 --- a/styles/src/themes/atelier/atelier-estuary-dark.ts +++ b/styles/src/themes/atelier/atelier-estuary-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Estuary Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-estuary-light.ts b/styles/src/themes/atelier/atelier-estuary-light.ts index 75fcb8e830..adc77e7607 100644 --- a/styles/src/themes/atelier/atelier-estuary-light.ts +++ b/styles/src/themes/atelier/atelier-estuary-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Estuary Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-forest-dark.ts b/styles/src/themes/atelier/atelier-forest-dark.ts index 7ee7ae4ab1..3e89518c0b 100644 --- a/styles/src/themes/atelier/atelier-forest-dark.ts +++ b/styles/src/themes/atelier/atelier-forest-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Forest Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-forest-light.ts b/styles/src/themes/atelier/atelier-forest-light.ts index 17d3b63d88..68d2c50876 100644 --- a/styles/src/themes/atelier/atelier-forest-light.ts +++ b/styles/src/themes/atelier/atelier-forest-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Forest Light`, author: meta.author, - appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + appearance: ThemeAppearance.Light, + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-heath-dark.ts b/styles/src/themes/atelier/atelier-heath-dark.ts index 11751367a3..c185d69e43 100644 --- a/styles/src/themes/atelier/atelier-heath-dark.ts +++ b/styles/src/themes/atelier/atelier-heath-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Heath Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-heath-light.ts b/styles/src/themes/atelier/atelier-heath-light.ts index 07f4a9b3cb..4414987e22 100644 --- a/styles/src/themes/atelier/atelier-heath-light.ts +++ b/styles/src/themes/atelier/atelier-heath-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Heath Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-lakeside-dark.ts b/styles/src/themes/atelier/atelier-lakeside-dark.ts index b1c98ddfdf..7fdc3b4eba 100644 --- a/styles/src/themes/atelier/atelier-lakeside-dark.ts +++ b/styles/src/themes/atelier/atelier-lakeside-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Lakeside Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-lakeside-light.ts b/styles/src/themes/atelier/atelier-lakeside-light.ts index d960444def..bdda48f6c7 100644 --- a/styles/src/themes/atelier/atelier-lakeside-light.ts +++ b/styles/src/themes/atelier/atelier-lakeside-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Lakeside Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-plateau-dark.ts b/styles/src/themes/atelier/atelier-plateau-dark.ts index 74693b24fd..ff287bc80d 100644 --- a/styles/src/themes/atelier/atelier-plateau-dark.ts +++ b/styles/src/themes/atelier/atelier-plateau-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Plateau Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-plateau-light.ts b/styles/src/themes/atelier/atelier-plateau-light.ts index dd3130cea0..8a9fb989ad 100644 --- a/styles/src/themes/atelier/atelier-plateau-light.ts +++ b/styles/src/themes/atelier/atelier-plateau-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Plateau Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-savanna-dark.ts b/styles/src/themes/atelier/atelier-savanna-dark.ts index c387ac5ae9..d94af30334 100644 --- a/styles/src/themes/atelier/atelier-savanna-dark.ts +++ b/styles/src/themes/atelier/atelier-savanna-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Savanna Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-savanna-light.ts b/styles/src/themes/atelier/atelier-savanna-light.ts index 64edd406a8..2426b05400 100644 --- a/styles/src/themes/atelier/atelier-savanna-light.ts +++ b/styles/src/themes/atelier/atelier-savanna-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Savanna Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-seaside-dark.ts b/styles/src/themes/atelier/atelier-seaside-dark.ts index dbccb96013..abb267f5a4 100644 --- a/styles/src/themes/atelier/atelier-seaside-dark.ts +++ b/styles/src/themes/atelier/atelier-seaside-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Seaside Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-seaside-light.ts b/styles/src/themes/atelier/atelier-seaside-light.ts index a9c034ed44..455e7795e1 100644 --- a/styles/src/themes/atelier/atelier-seaside-light.ts +++ b/styles/src/themes/atelier/atelier-seaside-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Seaside Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-sulphurpool-dark.ts b/styles/src/themes/atelier/atelier-sulphurpool-dark.ts index edfc518b8e..3f33647daa 100644 --- a/styles/src/themes/atelier/atelier-sulphurpool-dark.ts +++ b/styles/src/themes/atelier/atelier-sulphurpool-dark.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Sulphurpool Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ colors.base00, colors.base01, @@ -45,17 +45,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base06, colors.base07, ]), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/atelier-sulphurpool-light.ts b/styles/src/themes/atelier/atelier-sulphurpool-light.ts index fbef6683bf..2cb4d04539 100644 --- a/styles/src/themes/atelier/atelier-sulphurpool-light.ts +++ b/styles/src/themes/atelier/atelier-sulphurpool-light.ts @@ -1,5 +1,5 @@ -import { chroma, ThemeAppearance, ThemeConfig, colorRamp } from "../../common" -import { meta, buildSyntax, Variant } from "./common" +import { chroma, ThemeAppearance, ThemeConfig, color_ramp } from "../../common" +import { meta, build_syntax, Variant } from "./common" const variant: Variant = { colors: { @@ -22,19 +22,19 @@ const variant: Variant = { }, } -const syntax = buildSyntax(variant) +const syntax = build_syntax(variant) -const getTheme = (variant: Variant): ThemeConfig => { +const get_theme = (variant: Variant): ThemeConfig => { const { colors } = variant return { name: `${meta.name} Sulphurpool Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale( [ colors.base00, @@ -47,17 +47,17 @@ const getTheme = (variant: Variant): ThemeConfig => { colors.base07, ].reverse() ), - red: colorRamp(chroma(colors.base08)), - orange: colorRamp(chroma(colors.base09)), - yellow: colorRamp(chroma(colors.base0A)), - green: colorRamp(chroma(colors.base0B)), - cyan: colorRamp(chroma(colors.base0C)), - blue: colorRamp(chroma(colors.base0D)), - violet: colorRamp(chroma(colors.base0E)), - magenta: colorRamp(chroma(colors.base0F)), + red: color_ramp(chroma(colors.base08)), + orange: color_ramp(chroma(colors.base09)), + yellow: color_ramp(chroma(colors.base0A)), + green: color_ramp(chroma(colors.base0B)), + cyan: color_ramp(chroma(colors.base0C)), + blue: color_ramp(chroma(colors.base0D)), + violet: color_ramp(chroma(colors.base0E)), + magenta: color_ramp(chroma(colors.base0F)), }, override: { syntax }, } } -export const theme = getTheme(variant) +export const theme = get_theme(variant) diff --git a/styles/src/themes/atelier/common.ts b/styles/src/themes/atelier/common.ts index 2e5be61f52..b76ccc5b60 100644 --- a/styles/src/themes/atelier/common.ts +++ b/styles/src/themes/atelier/common.ts @@ -24,12 +24,12 @@ export interface Variant { export const meta: ThemeFamilyMeta = { name: "Atelier", author: "Bram de Haan (http://atelierbramdehaan.nl)", - licenseType: ThemeLicenseType.MIT, - licenseUrl: + license_type: ThemeLicenseType.MIT, + license_url: "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", } -export const buildSyntax = (variant: Variant): ThemeSyntax => { +export const build_syntax = (variant: Variant): ThemeSyntax => { const { colors } = variant return { primary: { color: colors.base06 }, diff --git a/styles/src/themes/ayu/LICENSE b/styles/src/themes/ayu/LICENSE index 6b83ef0582..37a9229268 100644 --- a/styles/src/themes/ayu/LICENSE +++ b/styles/src/themes/ayu/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/ayu/ayu-dark.ts b/styles/src/themes/ayu/ayu-dark.ts index 7feddacd2b..a12ce08e29 100644 --- a/styles/src/themes/ayu/ayu-dark.ts +++ b/styles/src/themes/ayu/ayu-dark.ts @@ -1,16 +1,16 @@ import { ThemeAppearance, ThemeConfig } from "../../common" -import { ayu, meta, buildTheme } from "./common" +import { ayu, meta, build_theme } from "./common" const variant = ayu.dark -const { ramps, syntax } = buildTheme(variant, false) +const { ramps, syntax } = build_theme(variant, false) export const theme: ThemeConfig = { name: `${meta.name} Dark`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: ramps, + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: ramps, override: { syntax }, } diff --git a/styles/src/themes/ayu/ayu-light.ts b/styles/src/themes/ayu/ayu-light.ts index bf02385247..aceda0d017 100644 --- a/styles/src/themes/ayu/ayu-light.ts +++ b/styles/src/themes/ayu/ayu-light.ts @@ -1,16 +1,16 @@ import { ThemeAppearance, ThemeConfig } from "../../common" -import { ayu, meta, buildTheme } from "./common" +import { ayu, meta, build_theme } from "./common" const variant = ayu.light -const { ramps, syntax } = buildTheme(variant, true) +const { ramps, syntax } = build_theme(variant, true) export const theme: ThemeConfig = { name: `${meta.name} Light`, author: meta.author, appearance: ThemeAppearance.Light, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: ramps, + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: ramps, override: { syntax }, } diff --git a/styles/src/themes/ayu/ayu-mirage.ts b/styles/src/themes/ayu/ayu-mirage.ts index d2a69e7ab6..9dd3ea7a61 100644 --- a/styles/src/themes/ayu/ayu-mirage.ts +++ b/styles/src/themes/ayu/ayu-mirage.ts @@ -1,16 +1,16 @@ import { ThemeAppearance, ThemeConfig } from "../../common" -import { ayu, meta, buildTheme } from "./common" +import { ayu, meta, build_theme } from "./common" const variant = ayu.mirage -const { ramps, syntax } = buildTheme(variant, false) +const { ramps, syntax } = build_theme(variant, false) export const theme: ThemeConfig = { name: `${meta.name} Mirage`, author: meta.author, appearance: ThemeAppearance.Dark, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: ramps, + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: ramps, override: { syntax }, } diff --git a/styles/src/themes/ayu/common.ts b/styles/src/themes/ayu/common.ts index 1eb2c91916..2bd0bbf259 100644 --- a/styles/src/themes/ayu/common.ts +++ b/styles/src/themes/ayu/common.ts @@ -1,7 +1,7 @@ import { dark, light, mirage } from "ayu" import { chroma, - colorRamp, + color_ramp, ThemeLicenseType, ThemeSyntax, ThemeFamilyMeta, @@ -13,16 +13,16 @@ export const ayu = { mirage, } -export const buildTheme = (t: typeof dark, light: boolean) => { +export const build_theme = (t: typeof dark, light: boolean) => { const color = { - lightBlue: t.syntax.tag.hex(), + light_blue: t.syntax.tag.hex(), yellow: t.syntax.func.hex(), blue: t.syntax.entity.hex(), green: t.syntax.string.hex(), teal: t.syntax.regexp.hex(), red: t.syntax.markup.hex(), orange: t.syntax.keyword.hex(), - lightYellow: t.syntax.special.hex(), + light_yellow: t.syntax.special.hex(), gray: t.syntax.comment.hex(), purple: t.syntax.constant.hex(), } @@ -48,20 +48,20 @@ export const buildTheme = (t: typeof dark, light: boolean) => { light ? t.editor.fg.hex() : t.editor.bg.hex(), light ? t.editor.bg.hex() : t.editor.fg.hex(), ]), - red: colorRamp(chroma(color.red)), - orange: colorRamp(chroma(color.orange)), - yellow: colorRamp(chroma(color.yellow)), - green: colorRamp(chroma(color.green)), - cyan: colorRamp(chroma(color.teal)), - blue: colorRamp(chroma(color.blue)), - violet: colorRamp(chroma(color.purple)), - magenta: colorRamp(chroma(color.lightBlue)), + red: color_ramp(chroma(color.red)), + orange: color_ramp(chroma(color.orange)), + yellow: color_ramp(chroma(color.yellow)), + green: color_ramp(chroma(color.green)), + cyan: color_ramp(chroma(color.teal)), + blue: color_ramp(chroma(color.blue)), + violet: color_ramp(chroma(color.purple)), + magenta: color_ramp(chroma(color.light_blue)), }, syntax, } } -export const buildSyntax = (t: typeof dark): ThemeSyntax => { +export const build_syntax = (t: typeof dark): ThemeSyntax => { return { constant: { color: t.syntax.constant.hex() }, "string.regex": { color: t.syntax.regexp.hex() }, @@ -80,6 +80,6 @@ export const buildSyntax = (t: typeof dark): ThemeSyntax => { export const meta: ThemeFamilyMeta = { name: "Ayu", author: "dempfi", - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/dempfi/ayu", + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/dempfi/ayu", } diff --git a/styles/src/themes/gruvbox/LICENSE b/styles/src/themes/gruvbox/LICENSE index 2a92306143..0e18d6d7a9 100644 --- a/styles/src/themes/gruvbox/LICENSE +++ b/styles/src/themes/gruvbox/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/gruvbox/gruvbox-common.ts b/styles/src/themes/gruvbox/gruvbox-common.ts index 18e8c5b97e..2fa6b58faa 100644 --- a/styles/src/themes/gruvbox/gruvbox-common.ts +++ b/styles/src/themes/gruvbox/gruvbox-common.ts @@ -1,6 +1,6 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, @@ -11,8 +11,8 @@ import { const meta: ThemeFamilyMeta = { name: "Gruvbox", author: "morhetz ", - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/morhetz/gruvbox", + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/morhetz/gruvbox", } const color = { @@ -73,7 +73,7 @@ interface ThemeColors { gray: string } -const darkNeutrals = [ +const dark_neutrals = [ color.dark1, color.dark2, color.dark3, @@ -96,7 +96,7 @@ const dark: ThemeColors = { gray: color.light4, } -const lightNeutrals = [ +const light_neutrals = [ color.light1, color.light2, color.light3, @@ -119,14 +119,6 @@ const light: ThemeColors = { gray: color.dark4, } -const darkHardNeutral = [color.dark0_hard, ...darkNeutrals] -const darkNeutral = [color.dark0, ...darkNeutrals] -const darkSoftNeutral = [color.dark0_soft, ...darkNeutrals] - -const lightHardNeutral = [color.light0_hard, ...lightNeutrals] -const lightNeutral = [color.light0, ...lightNeutrals] -const lightSoftNeutral = [color.light0_soft, ...lightNeutrals] - interface Variant { name: string appearance: "light" | "dark" @@ -167,61 +159,68 @@ const variant: Variant[] = [ }, ] -const buildVariant = (variant: Variant): ThemeConfig => { +const dark_hard_neutral = [color.dark0_hard, ...dark_neutrals] +const dark_neutral = [color.dark0, ...dark_neutrals] +const dark_soft_neutral = [color.dark0_soft, ...dark_neutrals] + +const light_hard_neutral = [color.light0_hard, ...light_neutrals] +const light_neutral = [color.light0, ...light_neutrals] +const light_soft_neutral = [color.light0_soft, ...light_neutrals] + +const build_variant = (variant: Variant): ThemeConfig => { const { colors } = variant const name = `Gruvbox ${variant.name}` - const isLight = variant.appearance === "light" + const is_light = variant.appearance === "light" let neutral: string[] = [] switch (variant.name) { - case "Dark Hard": { - neutral = darkHardNeutral + case "Dark Hard": + neutral = dark_hard_neutral break - } - case "Dark": { - neutral = darkNeutral + + case "Dark": + neutral = dark_neutral break - } - case "Dark Soft": { - neutral = darkSoftNeutral + + case "Dark Soft": + neutral = dark_soft_neutral break - } - case "Light Hard": { - neutral = lightHardNeutral + + case "Light Hard": + neutral = light_hard_neutral break - } - case "Light": { - neutral = lightNeutral + + case "Light": + neutral = light_neutral break - } - case "Light Soft": { - neutral = lightSoftNeutral + + case "Light Soft": + neutral = light_soft_neutral break - } } const ramps = { - neutral: chroma.scale(isLight ? neutral.reverse() : neutral), - red: colorRamp(chroma(variant.colors.red)), - orange: colorRamp(chroma(variant.colors.orange)), - yellow: colorRamp(chroma(variant.colors.yellow)), - green: colorRamp(chroma(variant.colors.green)), - cyan: colorRamp(chroma(variant.colors.aqua)), - blue: colorRamp(chroma(variant.colors.blue)), - violet: colorRamp(chroma(variant.colors.purple)), - magenta: colorRamp(chroma(variant.colors.gray)), + neutral: chroma.scale(is_light ? neutral.reverse() : neutral), + red: color_ramp(chroma(variant.colors.red)), + orange: color_ramp(chroma(variant.colors.orange)), + yellow: color_ramp(chroma(variant.colors.yellow)), + green: color_ramp(chroma(variant.colors.green)), + cyan: color_ramp(chroma(variant.colors.aqua)), + blue: color_ramp(chroma(variant.colors.blue)), + violet: color_ramp(chroma(variant.colors.purple)), + magenta: color_ramp(chroma(variant.colors.gray)), } const syntax: ThemeSyntax = { - primary: { color: neutral[isLight ? 0 : 8] }, + primary: { color: neutral[is_light ? 0 : 8] }, "text.literal": { color: colors.blue }, comment: { color: colors.gray }, - punctuation: { color: neutral[isLight ? 1 : 7] }, - "punctuation.bracket": { color: neutral[isLight ? 3 : 5] }, - "punctuation.list_marker": { color: neutral[isLight ? 0 : 8] }, + punctuation: { color: neutral[is_light ? 1 : 7] }, + "punctuation.bracket": { color: neutral[is_light ? 3 : 5] }, + "punctuation.list_marker": { color: neutral[is_light ? 0 : 8] }, operator: { color: colors.aqua }, boolean: { color: colors.purple }, number: { color: colors.purple }, @@ -237,10 +236,10 @@ const buildVariant = (variant: Variant): ThemeConfig => { function: { color: colors.green }, "function.builtin": { color: colors.red }, variable: { color: colors.blue }, - property: { color: neutral[isLight ? 0 : 8] }, + property: { color: neutral[is_light ? 0 : 8] }, embedded: { color: colors.aqua }, - linkText: { color: colors.aqua }, - linkUri: { color: colors.purple }, + link_text: { color: colors.aqua }, + link_uri: { color: colors.purple }, title: { color: colors.green }, } @@ -248,18 +247,18 @@ const buildVariant = (variant: Variant): ThemeConfig => { name, author: meta.author, appearance: variant.appearance as ThemeAppearance, - licenseType: meta.licenseType, - licenseUrl: meta.licenseUrl, - licenseFile: `${__dirname}/LICENSE`, - inputColor: ramps, + license_type: meta.license_type, + license_url: meta.license_url, + license_file: `${__dirname}/LICENSE`, + input_color: ramps, override: { syntax }, } } // Variants -export const darkHard = buildVariant(variant[0]) -export const darkDefault = buildVariant(variant[1]) -export const darkSoft = buildVariant(variant[2]) -export const lightHard = buildVariant(variant[3]) -export const lightDefault = buildVariant(variant[4]) -export const lightSoft = buildVariant(variant[5]) +export const dark_hard = build_variant(variant[0]) +export const dark_default = build_variant(variant[1]) +export const dark_soft = build_variant(variant[2]) +export const light_hard = build_variant(variant[3]) +export const light_default = build_variant(variant[4]) +export const light_soft = build_variant(variant[5]) diff --git a/styles/src/themes/gruvbox/gruvbox-dark-hard.ts b/styles/src/themes/gruvbox/gruvbox-dark-hard.ts index 4102671189..72757c99f2 100644 --- a/styles/src/themes/gruvbox/gruvbox-dark-hard.ts +++ b/styles/src/themes/gruvbox/gruvbox-dark-hard.ts @@ -1 +1 @@ -export { darkHard } from "./gruvbox-common" +export { dark_hard } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-dark-soft.ts b/styles/src/themes/gruvbox/gruvbox-dark-soft.ts index d550d63768..d8f63ed331 100644 --- a/styles/src/themes/gruvbox/gruvbox-dark-soft.ts +++ b/styles/src/themes/gruvbox/gruvbox-dark-soft.ts @@ -1 +1 @@ -export { darkSoft } from "./gruvbox-common" +export { dark_soft } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-dark.ts b/styles/src/themes/gruvbox/gruvbox-dark.ts index 05850028a4..0582baa0d8 100644 --- a/styles/src/themes/gruvbox/gruvbox-dark.ts +++ b/styles/src/themes/gruvbox/gruvbox-dark.ts @@ -1 +1 @@ -export { darkDefault } from "./gruvbox-common" +export { dark_default } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-light-hard.ts b/styles/src/themes/gruvbox/gruvbox-light-hard.ts index ec3cddec75..bcaea06a41 100644 --- a/styles/src/themes/gruvbox/gruvbox-light-hard.ts +++ b/styles/src/themes/gruvbox/gruvbox-light-hard.ts @@ -1 +1 @@ -export { lightHard } from "./gruvbox-common" +export { light_hard } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-light-soft.ts b/styles/src/themes/gruvbox/gruvbox-light-soft.ts index 0888e847ac..5eb79f647b 100644 --- a/styles/src/themes/gruvbox/gruvbox-light-soft.ts +++ b/styles/src/themes/gruvbox/gruvbox-light-soft.ts @@ -1 +1 @@ -export { lightSoft } from "./gruvbox-common" +export { light_soft } from "./gruvbox-common" diff --git a/styles/src/themes/gruvbox/gruvbox-light.ts b/styles/src/themes/gruvbox/gruvbox-light.ts index 6f53ce5299..dc54a33f26 100644 --- a/styles/src/themes/gruvbox/gruvbox-light.ts +++ b/styles/src/themes/gruvbox/gruvbox-light.ts @@ -1 +1 @@ -export { lightDefault } from "./gruvbox-common" +export { light_default } from "./gruvbox-common" diff --git a/styles/src/themes/index.ts b/styles/src/themes/index.ts index 75853bc042..72bb100e7b 100644 --- a/styles/src/themes/index.ts +++ b/styles/src/themes/index.ts @@ -1,82 +1,82 @@ import { ThemeConfig } from "../theme" -import { darkDefault as gruvboxDark } from "./gruvbox/gruvbox-dark" -import { darkHard as gruvboxDarkHard } from "./gruvbox/gruvbox-dark-hard" -import { darkSoft as gruvboxDarkSoft } from "./gruvbox/gruvbox-dark-soft" -import { lightDefault as gruvboxLight } from "./gruvbox/gruvbox-light" -import { lightHard as gruvboxLightHard } from "./gruvbox/gruvbox-light-hard" -import { lightSoft as gruvboxLightSoft } from "./gruvbox/gruvbox-light-soft" -import { dark as solarizedDark } from "./solarized/solarized" -import { light as solarizedLight } from "./solarized/solarized" -import { dark as andromedaDark } from "./andromeda/andromeda" -import { theme as oneDark } from "./one/one-dark" -import { theme as oneLight } from "./one/one-light" -import { theme as ayuLight } from "./ayu/ayu-light" -import { theme as ayuDark } from "./ayu/ayu-dark" -import { theme as ayuMirage } from "./ayu/ayu-mirage" -import { theme as rosePine } from "./rose-pine/rose-pine" -import { theme as rosePineDawn } from "./rose-pine/rose-pine-dawn" -import { theme as rosePineMoon } from "./rose-pine/rose-pine-moon" +import { dark_default as gruvbox_dark } from "./gruvbox/gruvbox-dark" +import { dark_hard as gruvbox_dark_hard } from "./gruvbox/gruvbox-dark-hard" +import { dark_soft as gruvbox_dark_soft } from "./gruvbox/gruvbox-dark-soft" +import { light_default as gruvbox_light } from "./gruvbox/gruvbox-light" +import { light_hard as gruvbox_light_hard } from "./gruvbox/gruvbox-light-hard" +import { light_soft as gruvbox_light_soft } from "./gruvbox/gruvbox-light-soft" +import { dark as solarized_dark } from "./solarized/solarized" +import { light as solarized_light } from "./solarized/solarized" +import { dark as andromeda_dark } from "./andromeda/andromeda" +import { theme as one_dark } from "./one/one-dark" +import { theme as one_light } from "./one/one-light" +import { theme as ayu_light } from "./ayu/ayu-light" +import { theme as ayu_dark } from "./ayu/ayu-dark" +import { theme as ayu_mirage } from "./ayu/ayu-mirage" +import { theme as rose_pine } from "./rose-pine/rose-pine" +import { theme as rose_pine_dawn } from "./rose-pine/rose-pine-dawn" +import { theme as rose_pine_moon } from "./rose-pine/rose-pine-moon" import { theme as sandcastle } from "./sandcastle/sandcastle" import { theme as summercamp } from "./summercamp/summercamp" -import { theme as atelierCaveDark } from "./atelier/atelier-cave-dark" -import { theme as atelierCaveLight } from "./atelier/atelier-cave-light" -import { theme as atelierDuneDark } from "./atelier/atelier-dune-dark" -import { theme as atelierDuneLight } from "./atelier/atelier-dune-light" -import { theme as atelierEstuaryDark } from "./atelier/atelier-estuary-dark" -import { theme as atelierEstuaryLight } from "./atelier/atelier-estuary-light" -import { theme as atelierForestDark } from "./atelier/atelier-forest-dark" -import { theme as atelierForestLight } from "./atelier/atelier-forest-light" -import { theme as atelierHeathDark } from "./atelier/atelier-heath-dark" -import { theme as atelierHeathLight } from "./atelier/atelier-heath-light" -import { theme as atelierLakesideDark } from "./atelier/atelier-lakeside-dark" -import { theme as atelierLakesideLight } from "./atelier/atelier-lakeside-light" -import { theme as atelierPlateauDark } from "./atelier/atelier-plateau-dark" -import { theme as atelierPlateauLight } from "./atelier/atelier-plateau-light" -import { theme as atelierSavannaDark } from "./atelier/atelier-savanna-dark" -import { theme as atelierSavannaLight } from "./atelier/atelier-savanna-light" -import { theme as atelierSeasideDark } from "./atelier/atelier-seaside-dark" -import { theme as atelierSeasideLight } from "./atelier/atelier-seaside-light" -import { theme as atelierSulphurpoolDark } from "./atelier/atelier-sulphurpool-dark" -import { theme as atelierSulphurpoolLight } from "./atelier/atelier-sulphurpool-light" +import { theme as atelier_cave_dark } from "./atelier/atelier-cave-dark" +import { theme as atelier_cave_light } from "./atelier/atelier-cave-light" +import { theme as atelier_dune_dark } from "./atelier/atelier-dune-dark" +import { theme as atelier_dune_light } from "./atelier/atelier-dune-light" +import { theme as atelier_estuary_dark } from "./atelier/atelier-estuary-dark" +import { theme as atelier_estuary_light } from "./atelier/atelier-estuary-light" +import { theme as atelier_forest_dark } from "./atelier/atelier-forest-dark" +import { theme as atelier_forest_light } from "./atelier/atelier-forest-light" +import { theme as atelier_heath_dark } from "./atelier/atelier-heath-dark" +import { theme as atelier_heath_light } from "./atelier/atelier-heath-light" +import { theme as atelier_lakeside_dark } from "./atelier/atelier-lakeside-dark" +import { theme as atelier_lakeside_light } from "./atelier/atelier-lakeside-light" +import { theme as atelier_plateau_dark } from "./atelier/atelier-plateau-dark" +import { theme as atelier_plateau_light } from "./atelier/atelier-plateau-light" +import { theme as atelier_savanna_dark } from "./atelier/atelier-savanna-dark" +import { theme as atelier_savanna_light } from "./atelier/atelier-savanna-light" +import { theme as atelier_seaside_dark } from "./atelier/atelier-seaside-dark" +import { theme as atelier_seaside_light } from "./atelier/atelier-seaside-light" +import { theme as atelier_sulphurpool_dark } from "./atelier/atelier-sulphurpool-dark" +import { theme as atelier_sulphurpool_light } from "./atelier/atelier-sulphurpool-light" export const themes: ThemeConfig[] = [ - oneDark, - oneLight, - ayuLight, - ayuDark, - ayuMirage, - gruvboxDark, - gruvboxDarkHard, - gruvboxDarkSoft, - gruvboxLight, - gruvboxLightHard, - gruvboxLightSoft, - rosePine, - rosePineDawn, - rosePineMoon, + one_dark, + one_light, + ayu_light, + ayu_dark, + ayu_mirage, + gruvbox_dark, + gruvbox_dark_hard, + gruvbox_dark_soft, + gruvbox_light, + gruvbox_light_hard, + gruvbox_light_soft, + rose_pine, + rose_pine_dawn, + rose_pine_moon, sandcastle, - solarizedDark, - solarizedLight, - andromedaDark, + solarized_dark, + solarized_light, + andromeda_dark, summercamp, - atelierCaveDark, - atelierCaveLight, - atelierDuneDark, - atelierDuneLight, - atelierEstuaryDark, - atelierEstuaryLight, - atelierForestDark, - atelierForestLight, - atelierHeathDark, - atelierHeathLight, - atelierLakesideDark, - atelierLakesideLight, - atelierPlateauDark, - atelierPlateauLight, - atelierSavannaDark, - atelierSavannaLight, - atelierSeasideDark, - atelierSeasideLight, - atelierSulphurpoolDark, - atelierSulphurpoolLight, + atelier_cave_dark, + atelier_cave_light, + atelier_dune_dark, + atelier_dune_light, + atelier_estuary_dark, + atelier_estuary_light, + atelier_forest_dark, + atelier_forest_light, + atelier_heath_dark, + atelier_heath_light, + atelier_lakeside_dark, + atelier_lakeside_light, + atelier_plateau_dark, + atelier_plateau_light, + atelier_savanna_dark, + atelier_savanna_light, + atelier_seaside_dark, + atelier_seaside_light, + atelier_sulphurpool_dark, + atelier_sulphurpool_light, ] diff --git a/styles/src/themes/one/LICENSE b/styles/src/themes/one/LICENSE index dc07dc10ad..f7637d33ea 100644 --- a/styles/src/themes/one/LICENSE +++ b/styles/src/themes/one/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/one/one-dark.ts b/styles/src/themes/one/one-dark.ts index 69a5bd5575..f672b892ee 100644 --- a/styles/src/themes/one/one-dark.ts +++ b/styles/src/themes/one/one-dark.ts @@ -1,7 +1,7 @@ import { chroma, - fontWeights, - colorRamp, + font_weights, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, @@ -11,7 +11,7 @@ const color = { white: "#ACB2BE", grey: "#5D636F", red: "#D07277", - darkRed: "#B1574B", + dark_red: "#B1574B", orange: "#C0966B", yellow: "#DFC184", green: "#A1C181", @@ -24,10 +24,11 @@ export const theme: ThemeConfig = { name: "One Dark", author: "simurai", appearance: ThemeAppearance.Dark, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/atom/atom/tree/master/packages/one-dark-ui", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: ThemeLicenseType.MIT, + license_url: + "https://github.com/atom/atom/tree/master/packages/one-dark-ui", + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma .scale([ "#282c34", @@ -40,14 +41,14 @@ export const theme: ThemeConfig = { "#c8ccd4", ]) .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]), - red: colorRamp(chroma(color.red)), - orange: colorRamp(chroma(color.orange)), - yellow: colorRamp(chroma(color.yellow)), - green: colorRamp(chroma(color.green)), - cyan: colorRamp(chroma(color.teal)), - blue: colorRamp(chroma(color.blue)), - violet: colorRamp(chroma(color.purple)), - magenta: colorRamp(chroma("#be5046")), + red: color_ramp(chroma(color.red)), + orange: color_ramp(chroma(color.orange)), + yellow: color_ramp(chroma(color.yellow)), + green: color_ramp(chroma(color.green)), + cyan: color_ramp(chroma(color.teal)), + blue: color_ramp(chroma(color.blue)), + violet: color_ramp(chroma(color.purple)), + magenta: color_ramp(chroma("#be5046")), }, override: { syntax: { @@ -57,8 +58,8 @@ export const theme: ThemeConfig = { "emphasis.strong": { color: color.orange }, function: { color: color.blue }, keyword: { color: color.purple }, - linkText: { color: color.blue, italic: false }, - linkUri: { color: color.teal }, + link_text: { color: color.blue, italic: false }, + link_uri: { color: color.teal }, number: { color: color.orange }, constant: { color: color.yellow }, operator: { color: color.teal }, @@ -66,9 +67,9 @@ export const theme: ThemeConfig = { property: { color: color.red }, punctuation: { color: color.white }, "punctuation.list_marker": { color: color.red }, - "punctuation.special": { color: color.darkRed }, + "punctuation.special": { color: color.dark_red }, string: { color: color.green }, - title: { color: color.red, weight: fontWeights.normal }, + title: { color: color.red, weight: font_weights.normal }, "text.literal": { color: color.green }, type: { color: color.teal }, "variable.special": { color: color.orange }, diff --git a/styles/src/themes/one/one-light.ts b/styles/src/themes/one/one-light.ts index 9123c8879d..c3de7826c9 100644 --- a/styles/src/themes/one/one-light.ts +++ b/styles/src/themes/one/one-light.ts @@ -1,7 +1,7 @@ import { chroma, - fontWeights, - colorRamp, + font_weights, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, @@ -11,7 +11,7 @@ const color = { black: "#383A41", grey: "#A2A3A7", red: "#D36050", - darkRed: "#B92C46", + dark_red: "#B92C46", orange: "#AD6F26", yellow: "#DFC184", green: "#659F58", @@ -25,11 +25,11 @@ export const theme: ThemeConfig = { name: "One Light", author: "simurai", appearance: ThemeAppearance.Light, - licenseType: ThemeLicenseType.MIT, - licenseUrl: + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/atom/atom/tree/master/packages/one-light-ui", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma .scale([ "#383A41", @@ -42,14 +42,14 @@ export const theme: ThemeConfig = { "#FAFAFA", ]) .domain([0.05, 0.22, 0.25, 0.45, 0.62, 0.8, 0.9, 1]), - red: colorRamp(chroma(color.red)), - orange: colorRamp(chroma(color.orange)), - yellow: colorRamp(chroma(color.yellow)), - green: colorRamp(chroma(color.green)), - cyan: colorRamp(chroma(color.teal)), - blue: colorRamp(chroma(color.blue)), - violet: colorRamp(chroma(color.purple)), - magenta: colorRamp(chroma(color.magenta)), + red: color_ramp(chroma(color.red)), + orange: color_ramp(chroma(color.orange)), + yellow: color_ramp(chroma(color.yellow)), + green: color_ramp(chroma(color.green)), + cyan: color_ramp(chroma(color.teal)), + blue: color_ramp(chroma(color.blue)), + violet: color_ramp(chroma(color.purple)), + magenta: color_ramp(chroma(color.magenta)), }, override: { syntax: { @@ -59,17 +59,17 @@ export const theme: ThemeConfig = { "emphasis.strong": { color: color.orange }, function: { color: color.blue }, keyword: { color: color.purple }, - linkText: { color: color.blue }, - linkUri: { color: color.teal }, + link_text: { color: color.blue }, + link_uri: { color: color.teal }, number: { color: color.orange }, operator: { color: color.teal }, primary: { color: color.black }, property: { color: color.red }, punctuation: { color: color.black }, "punctuation.list_marker": { color: color.red }, - "punctuation.special": { color: color.darkRed }, + "punctuation.special": { color: color.dark_red }, string: { color: color.green }, - title: { color: color.red, weight: fontWeights.normal }, + title: { color: color.red, weight: font_weights.normal }, "text.literal": { color: color.green }, type: { color: color.teal }, "variable.special": { color: color.orange }, diff --git a/styles/src/themes/rose-pine/LICENSE b/styles/src/themes/rose-pine/LICENSE index dfd60136f9..1276733453 100644 --- a/styles/src/themes/rose-pine/LICENSE +++ b/styles/src/themes/rose-pine/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/rose-pine/common.ts b/styles/src/themes/rose-pine/common.ts new file mode 100644 index 0000000000..5c5482a754 --- /dev/null +++ b/styles/src/themes/rose-pine/common.ts @@ -0,0 +1,75 @@ +import { ThemeSyntax } from "../../common" + +export const color = { + default: { + base: "#191724", + surface: "#1f1d2e", + overlay: "#26233a", + muted: "#6e6a86", + subtle: "#908caa", + text: "#e0def4", + love: "#eb6f92", + gold: "#f6c177", + rose: "#ebbcba", + pine: "#31748f", + foam: "#9ccfd8", + iris: "#c4a7e7", + highlight_low: "#21202e", + highlight_med: "#403d52", + highlight_high: "#524f67", + }, + moon: { + base: "#232136", + surface: "#2a273f", + overlay: "#393552", + muted: "#6e6a86", + subtle: "#908caa", + text: "#e0def4", + love: "#eb6f92", + gold: "#f6c177", + rose: "#ea9a97", + pine: "#3e8fb0", + foam: "#9ccfd8", + iris: "#c4a7e7", + highlight_low: "#2a283e", + highlight_med: "#44415a", + highlight_high: "#56526e", + }, + dawn: { + base: "#faf4ed", + surface: "#fffaf3", + overlay: "#f2e9e1", + muted: "#9893a5", + subtle: "#797593", + text: "#575279", + love: "#b4637a", + gold: "#ea9d34", + rose: "#d7827e", + pine: "#286983", + foam: "#56949f", + iris: "#907aa9", + highlight_low: "#f4ede8", + highlight_med: "#dfdad9", + highlight_high: "#cecacd", + }, +} + +export const syntax = (c: typeof color.default): Partial => { + return { + comment: { color: c.muted }, + operator: { color: c.pine }, + punctuation: { color: c.subtle }, + variable: { color: c.text }, + string: { color: c.gold }, + type: { color: c.foam }, + "type.builtin": { color: c.foam }, + boolean: { color: c.rose }, + function: { color: c.rose }, + keyword: { color: c.pine }, + tag: { color: c.foam }, + "function.method": { color: c.rose }, + title: { color: c.gold }, + link_text: { color: c.foam, italic: false }, + link_uri: { color: c.rose }, + } +} diff --git a/styles/src/themes/rose-pine/rose-pine-dawn.ts b/styles/src/themes/rose-pine/rose-pine-dawn.ts index a373ed378c..c78f1132dd 100644 --- a/styles/src/themes/rose-pine/rose-pine-dawn.ts +++ b/styles/src/themes/rose-pine/rose-pine-dawn.ts @@ -1,39 +1,49 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, } from "../../common" +import { color as c, syntax } from "./common" + +const color = c.dawn + +const green = chroma.mix(color.foam, "#10b981", 0.6, "lab") +const magenta = chroma.mix(color.love, color.pine, 0.5, "lab") + export const theme: ThemeConfig = { name: "Rosé Pine Dawn", author: "edunfelt", appearance: ThemeAppearance.Light, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/edunfelt/base16-rose-pine-scheme", + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma - .scale([ - "#575279", - "#797593", - "#9893A5", - "#B5AFB8", - "#D3CCCC", - "#F2E9E1", - "#FFFAF3", - "#FAF4ED", - ]) + .scale( + [ + color.base, + color.surface, + color.highlight_high, + color.overlay, + color.muted, + color.subtle, + color.text, + ].reverse() + ) .domain([0, 0.35, 0.45, 0.65, 0.7, 0.8, 0.9, 1]), - red: colorRamp(chroma("#B4637A")), - orange: colorRamp(chroma("#D7827E")), - yellow: colorRamp(chroma("#EA9D34")), - green: colorRamp(chroma("#679967")), - cyan: colorRamp(chroma("#286983")), - blue: colorRamp(chroma("#56949F")), - violet: colorRamp(chroma("#907AA9")), - magenta: colorRamp(chroma("#79549F")), + red: color_ramp(chroma(color.love)), + orange: color_ramp(chroma(color.iris)), + yellow: color_ramp(chroma(color.gold)), + green: color_ramp(chroma(green)), + cyan: color_ramp(chroma(color.pine)), + blue: color_ramp(chroma(color.foam)), + violet: color_ramp(chroma(color.iris)), + magenta: color_ramp(chroma(magenta)), + }, + override: { + syntax: syntax(color), }, - override: { syntax: {} }, } diff --git a/styles/src/themes/rose-pine/rose-pine-moon.ts b/styles/src/themes/rose-pine/rose-pine-moon.ts index 94b8166cb3..450d6865e7 100644 --- a/styles/src/themes/rose-pine/rose-pine-moon.ts +++ b/styles/src/themes/rose-pine/rose-pine-moon.ts @@ -1,39 +1,47 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, } from "../../common" +import { color as c, syntax } from "./common" + +const color = c.moon + +const green = chroma.mix(color.foam, "#10b981", 0.6, "lab") +const magenta = chroma.mix(color.love, color.pine, 0.5, "lab") + export const theme: ThemeConfig = { name: "Rosé Pine Moon", author: "edunfelt", appearance: ThemeAppearance.Dark, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/edunfelt/base16-rose-pine-scheme", + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma .scale([ - "#232136", - "#2A273F", - "#393552", - "#3E3A53", - "#56526C", - "#6E6A86", - "#908CAA", - "#E0DEF4", + color.base, + color.surface, + color.highlight_high, + color.overlay, + color.muted, + color.subtle, + color.text, ]) .domain([0, 0.3, 0.55, 1]), - red: colorRamp(chroma("#EB6F92")), - orange: colorRamp(chroma("#EBBCBA")), - yellow: colorRamp(chroma("#F6C177")), - green: colorRamp(chroma("#8DBD8D")), - cyan: colorRamp(chroma("#409BBE")), - blue: colorRamp(chroma("#9CCFD8")), - violet: colorRamp(chroma("#C4A7E7")), - magenta: colorRamp(chroma("#AB6FE9")), + red: color_ramp(chroma(color.love)), + orange: color_ramp(chroma(color.iris)), + yellow: color_ramp(chroma(color.gold)), + green: color_ramp(chroma(green)), + cyan: color_ramp(chroma(color.pine)), + blue: color_ramp(chroma(color.foam)), + violet: color_ramp(chroma(color.iris)), + magenta: color_ramp(chroma(magenta)), + }, + override: { + syntax: syntax(color), }, - override: { syntax: {} }, } diff --git a/styles/src/themes/rose-pine/rose-pine.ts b/styles/src/themes/rose-pine/rose-pine.ts index 3aabe3f10e..b305b5b577 100644 --- a/styles/src/themes/rose-pine/rose-pine.ts +++ b/styles/src/themes/rose-pine/rose-pine.ts @@ -1,37 +1,44 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, } from "../../common" +import { color as c, syntax } from "./common" + +const color = c.default + +const green = chroma.mix(color.foam, "#10b981", 0.6, "lab") +const magenta = chroma.mix(color.love, color.pine, 0.5, "lab") export const theme: ThemeConfig = { name: "Rosé Pine", author: "edunfelt", appearance: ThemeAppearance.Dark, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/edunfelt/base16-rose-pine-scheme", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/edunfelt/base16-rose-pine-scheme", + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ - "#191724", - "#1f1d2e", - "#26233A", - "#3E3A53", - "#56526C", - "#6E6A86", - "#908CAA", - "#E0DEF4", + color.base, + color.surface, + color.highlight_high, + color.overlay, + color.muted, + color.subtle, + color.text, ]), - red: colorRamp(chroma("#EB6F92")), - orange: colorRamp(chroma("#EBBCBA")), - yellow: colorRamp(chroma("#F6C177")), - green: colorRamp(chroma("#8DBD8D")), - cyan: colorRamp(chroma("#409BBE")), - blue: colorRamp(chroma("#9CCFD8")), - violet: colorRamp(chroma("#C4A7E7")), - magenta: colorRamp(chroma("#AB6FE9")), + red: color_ramp(chroma(color.love)), + orange: color_ramp(chroma(color.iris)), + yellow: color_ramp(chroma(color.gold)), + green: color_ramp(chroma(green)), + cyan: color_ramp(chroma(color.pine)), + blue: color_ramp(chroma(color.foam)), + violet: color_ramp(chroma(color.iris)), + magenta: color_ramp(chroma(magenta)), + }, + override: { + syntax: syntax(color), }, - override: { syntax: {} }, } diff --git a/styles/src/themes/sandcastle/LICENSE b/styles/src/themes/sandcastle/LICENSE index c66a06c51b..ba6559d810 100644 --- a/styles/src/themes/sandcastle/LICENSE +++ b/styles/src/themes/sandcastle/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/sandcastle/sandcastle.ts b/styles/src/themes/sandcastle/sandcastle.ts index 753828c665..b54c402e47 100644 --- a/styles/src/themes/sandcastle/sandcastle.ts +++ b/styles/src/themes/sandcastle/sandcastle.ts @@ -1,6 +1,6 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, @@ -10,10 +10,10 @@ export const theme: ThemeConfig = { name: "Sandcastle", author: "gessig", appearance: ThemeAppearance.Dark, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/gessig/base16-sandcastle-scheme", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/gessig/base16-sandcastle-scheme", + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma.scale([ "#282c34", "#2c323b", @@ -24,14 +24,14 @@ export const theme: ThemeConfig = { "#d5c4a1", "#fdf4c1", ]), - red: colorRamp(chroma("#B4637A")), - orange: colorRamp(chroma("#a07e3b")), - yellow: colorRamp(chroma("#a07e3b")), - green: colorRamp(chroma("#83a598")), - cyan: colorRamp(chroma("#83a598")), - blue: colorRamp(chroma("#528b8b")), - violet: colorRamp(chroma("#d75f5f")), - magenta: colorRamp(chroma("#a87322")), + red: color_ramp(chroma("#B4637A")), + orange: color_ramp(chroma("#a07e3b")), + yellow: color_ramp(chroma("#a07e3b")), + green: color_ramp(chroma("#83a598")), + cyan: color_ramp(chroma("#83a598")), + blue: color_ramp(chroma("#528b8b")), + violet: color_ramp(chroma("#d75f5f")), + magenta: color_ramp(chroma("#a87322")), }, override: { syntax: {} }, } diff --git a/styles/src/themes/solarized/LICENSE b/styles/src/themes/solarized/LICENSE index 221eee6f15..2b5ddc4158 100644 --- a/styles/src/themes/solarized/LICENSE +++ b/styles/src/themes/solarized/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/solarized/solarized.ts b/styles/src/themes/solarized/solarized.ts index 4084757525..05e6f018ab 100644 --- a/styles/src/themes/solarized/solarized.ts +++ b/styles/src/themes/solarized/solarized.ts @@ -1,6 +1,6 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, @@ -19,24 +19,24 @@ const ramps = { "#fdf6e3", ]) .domain([0, 0.2, 0.38, 0.45, 0.65, 0.7, 0.85, 1]), - red: colorRamp(chroma("#dc322f")), - orange: colorRamp(chroma("#cb4b16")), - yellow: colorRamp(chroma("#b58900")), - green: colorRamp(chroma("#859900")), - cyan: colorRamp(chroma("#2aa198")), - blue: colorRamp(chroma("#268bd2")), - violet: colorRamp(chroma("#6c71c4")), - magenta: colorRamp(chroma("#d33682")), + red: color_ramp(chroma("#dc322f")), + orange: color_ramp(chroma("#cb4b16")), + yellow: color_ramp(chroma("#b58900")), + green: color_ramp(chroma("#859900")), + cyan: color_ramp(chroma("#2aa198")), + blue: color_ramp(chroma("#268bd2")), + violet: color_ramp(chroma("#6c71c4")), + magenta: color_ramp(chroma("#d33682")), } export const dark: ThemeConfig = { name: "Solarized Dark", author: "Ethan Schoonover", appearance: ThemeAppearance.Dark, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/altercation/solarized", - licenseFile: `${__dirname}/LICENSE`, - inputColor: ramps, + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/altercation/solarized", + license_file: `${__dirname}/LICENSE`, + input_color: ramps, override: { syntax: {} }, } @@ -44,9 +44,9 @@ export const light: ThemeConfig = { name: "Solarized Light", author: "Ethan Schoonover", appearance: ThemeAppearance.Light, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/altercation/solarized", - licenseFile: `${__dirname}/LICENSE`, - inputColor: ramps, + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/altercation/solarized", + license_file: `${__dirname}/LICENSE`, + input_color: ramps, override: { syntax: {} }, } diff --git a/styles/src/themes/summercamp/LICENSE b/styles/src/themes/summercamp/LICENSE index d7525414ad..dd49a64536 100644 --- a/styles/src/themes/summercamp/LICENSE +++ b/styles/src/themes/summercamp/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/styles/src/themes/summercamp/summercamp.ts b/styles/src/themes/summercamp/summercamp.ts index 08098d2e2f..f9037feae4 100644 --- a/styles/src/themes/summercamp/summercamp.ts +++ b/styles/src/themes/summercamp/summercamp.ts @@ -1,6 +1,6 @@ import { chroma, - colorRamp, + color_ramp, ThemeAppearance, ThemeLicenseType, ThemeConfig, @@ -10,10 +10,10 @@ export const theme: ThemeConfig = { name: "Summercamp", author: "zoefiri", appearance: ThemeAppearance.Dark, - licenseType: ThemeLicenseType.MIT, - licenseUrl: "https://github.com/zoefiri/base16-sc", - licenseFile: `${__dirname}/LICENSE`, - inputColor: { + license_type: ThemeLicenseType.MIT, + license_url: "https://github.com/zoefiri/base16-sc", + license_file: `${__dirname}/LICENSE`, + input_color: { neutral: chroma .scale([ "#1c1810", @@ -26,14 +26,14 @@ export const theme: ThemeConfig = { "#f8f5de", ]) .domain([0, 0.2, 0.38, 0.4, 0.65, 0.7, 0.85, 1]), - red: colorRamp(chroma("#e35142")), - orange: colorRamp(chroma("#fba11b")), - yellow: colorRamp(chroma("#f2ff27")), - green: colorRamp(chroma("#5ceb5a")), - cyan: colorRamp(chroma("#5aebbc")), - blue: colorRamp(chroma("#489bf0")), - violet: colorRamp(chroma("#FF8080")), - magenta: colorRamp(chroma("#F69BE7")), + red: color_ramp(chroma("#e35142")), + orange: color_ramp(chroma("#fba11b")), + yellow: color_ramp(chroma("#f2ff27")), + green: color_ramp(chroma("#5ceb5a")), + cyan: color_ramp(chroma("#5aebbc")), + blue: color_ramp(chroma("#489bf0")), + violet: color_ramp(chroma("#FF8080")), + magenta: color_ramp(chroma("#F69BE7")), }, override: { syntax: {} }, } diff --git a/styles/src/utils/slugify.ts b/styles/src/utils/slugify.ts index 62b226cd10..04fd4d53bb 100644 --- a/styles/src/utils/slugify.ts +++ b/styles/src/utils/slugify.ts @@ -1 +1,10 @@ -export function slugify(t: string): string { return t.toString().toLowerCase().replace(/\s+/g, '-').replace(/[^\w\-]+/g, '').replace(/\-\-+/g, '-').replace(/^-+/, '').replace(/-+$/, '') } +export function slugify(t: string): string { + return t + .toString() + .toLowerCase() + .replace(/\s+/g, "-") + .replace(/[^\w-]+/g, "") + .replace(/--+/g, "-") + .replace(/^-+/, "") + .replace(/-+$/, "") +} diff --git a/styles/src/utils/snakeCase.ts b/styles/src/utils/snakeCase.ts deleted file mode 100644 index 5191064707..0000000000 --- a/styles/src/utils/snakeCase.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { snakeCase } from "case-anything" - -// https://stackoverflow.com/questions/60269936/typescript-convert-generic-object-from-snake-to-camel-case - -// Typescript magic to convert any string from camelCase to snake_case at compile time -type SnakeCase = S extends string - ? S extends `${infer T}${infer U}` - ? `${T extends Capitalize ? "_" : ""}${Lowercase}${SnakeCase}` - : S - : S - -type SnakeCased = { - [Property in keyof Type as SnakeCase]: SnakeCased -} - -export default function snakeCaseTree(object: T): SnakeCased { - const snakeObject: any = {} - for (const key in object) { - snakeObject[snakeCase(key, { keepSpecialCharacters: true })] = - snakeCaseValue(object[key]) - } - return snakeObject -} - -function snakeCaseValue(value: any): any { - if (typeof value === "object") { - if (Array.isArray(value)) { - return value.map(snakeCaseValue) - } else { - return snakeCaseTree(value) - } - } else { - return value - } -} diff --git a/styles/tsconfig.json b/styles/tsconfig.json index 6d24728a0a..281bd74b21 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -20,7 +20,11 @@ "noFallthroughCasesInSwitch": false, "experimentalDecorators": true, "strictPropertyInitialization": false, - "skipLibCheck": true + "skipLibCheck": true, + "useUnknownInCatchVariables": false, + "baseUrl": "." }, - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] } diff --git a/styles/vitest.config.ts b/styles/vitest.config.ts new file mode 100644 index 0000000000..00f3a9852d --- /dev/null +++ b/styles/vitest.config.ts @@ -0,0 +1,8 @@ +import { configDefaults, defineConfig } from "vitest/config" + +export default defineConfig({ + test: { + exclude: [...configDefaults.exclude, "target/*"], + include: ["src/**/*.{spec,test}.ts"], + }, +})