diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..35049cbcb13c204648d1f7897162492f05123199 --- /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 0000000000000000000000000000000000000000..b05d68911fb5f50afaa623649fd426f7eb1e7bbe --- /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 f1c16b2d4d2a540da2ff6bbd00f01323b820f12e..a906c8b82d6fd6fa77e68c66d83bfc2341cd422d 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 5a4d2ff25e19e7406f79c1b627510f4c94d58212..dbffa0f829cffe77c976a95d13f339be367e8840 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 24fd67f90ec17ecc7d0440fa9d6dc52996a939b2..ac089cee18e9161bb011f3135af3b619921e14cc 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 fca735596489a222208e2f65874efc4fcd04ffd5..1708ccfc0a81ec92ac626ef2a6816c55e16a44df 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 0000000000000000000000000000000000000000..3baf8df3e936347415749cf0667c04e32391f828 --- /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 0000000000000000000000000000000000000000..060caeecbfd58603530f253248a0c369ba329b4e --- /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 0000000000000000000000000000000000000000..be5eabd9b019902a44c03ac5545441702b6d7925 --- /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 0000000000000000000000000000000000000000..32d78f2d8da1c317727810706a892a63f588463e --- /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 0000000000000000000000000000000000000000..8feab7d44942915ef6d49602e272b03125ee8ea4 --- /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 0000000000000000000000000000000000000000..07213dc1ae61fbf49d3f72b107082b07892fa0c1 --- /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 0000000000000000000000000000000000000000..7d11c0cd5a6e11be048bcfc04c782fcd3e61f2ee --- /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 0000000000000000000000000000000000000000..69509a7d097821d2c0169ae468efc8d74a7e90c9 --- /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 0000000000000000000000000000000000000000..4f1b50cc4366775a792bef2b4475ec864856a3a7 --- /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 0000000000000000000000000000000000000000..caaec36477fbbf2bcfef558aa682092d0bbd9a01 --- /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 0000000000000000000000000000000000000000..18f1b6491233806086baf55ab67c5d7f4e10ff54 --- /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 0000000000000000000000000000000000000000..2d1d64ea4b82ef5e0d933b9bf0ec439c9998dd98 --- /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 0000000000000000000000000000000000000000..0d5dba095c7d0756d489d415276064a91d4fd3ce --- /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 0000000000000000000000000000000000000000..1b6b3f0ffa9c649b005739baafa9d973013af076 --- /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 0000000000000000000000000000000000000000..ada50e1079e481cde5f0f9ee5884a7030ebb0bc6 --- /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 0000000000000000000000000000000000000000..3cb28605cbf1b1a8470fabd1257370d74b3e5682 --- /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 0000000000000000000000000000000000000000..23db80f4dd0ebb04ee703fe74d4b535abbd01da1 --- /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 0000000000000000000000000000000000000000..07eaee7bf7d9274c402bb3f4bfaa0dea486eb09b --- /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 0000000000000000000000000000000000000000..8ace7df03f4d17ba1e8f858b94d418eb63618ea6 --- /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 0000000000000000000000000000000000000000..a0d93f3460ca940a1bf5e7ad94c46f56d40ccc7b --- /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 0000000000000000000000000000000000000000..74063f1d1e2346c09ee2a6a5297c30ef7e0c74ad --- /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 0000000000000000000000000000000000000000..7a4511aa2d69b39c305cd80c291c868007cba491 --- /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 0000000000000000000000000000000000000000..2ba9fef1019774f1e5094f5654d89df848cdbb5b --- /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 0000000000000000000000000000000000000000..5dc21a66890fb27f537b4400e96d48b7f7ce84a6 --- /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 0000000000000000000000000000000000000000..3a64c8394f0825b3708634c2d003a648877c35cd --- /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 0000000000000000000000000000000000000000..e3d30988d5e7b4547393281c7bdad60c3006f4f3 --- /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 0000000000000000000000000000000000000000..69fef41dee621d3f8cf681e630c0ce623d65124d --- /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 0000000000000000000000000000000000000000..c1016376e3232ead02dde954379ce74b7bfb68f7 --- /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 0000000000000000000000000000000000000000..ba426119e901d0a1132d0e47b34c0beebaec22ce --- /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 0000000000000000000000000000000000000000..0851f2e1e9f46d52cd2974b77a65e3a8b95b339e --- /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 0000000000000000000000000000000000000000..cb229c77fe827f64054b6bfa05f2ad2aaf17c2d3 --- /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 0000000000000000000000000000000000000000..a5c9cedbd32dd589c825852f447e8c6125c2a8fb --- /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 0000000000000000000000000000000000000000..aa764d4726f449c163b00e1bd993d12c5aa95c24 --- /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 0000000000000000000000000000000000000000..f8054781d9ec2ee79f0652ae20753e3e80752bff --- /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 0000000000000000000000000000000000000000..ea1c6dd42e8821b632f6de97d143a7b9f4b97fd2 --- /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 0000000000000000000000000000000000000000..bd58cf4ee38ee66e9860df11a9f4150899a9c8a8 --- /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 0000000000000000000000000000000000000000..5b725cd88dbf9337d52095a7567a2bc12e15439a --- /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 0000000000000000000000000000000000000000..90c4d827f13cd47a83a030c833a02e15492dc084 --- /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 0000000000000000000000000000000000000000..3bfde7d59baa675eeae72eac6f7245eadbe10821 --- /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 0000000000000000000000000000000000000000..f2d3c3d554e09837c464ff425c3af74413db4eb6 --- /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 0000000000000000000000000000000000000000..85fdcdfe5d7f3905f2056912a5bc56d229ca5ee0 --- /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 0000000000000000000000000000000000000000..5eb514ed2a60093e0c4eb904b4cc5c6d18b9a62f --- /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 0000000000000000000000000000000000000000..5deb197da51a7db874b57e1a473d4287b2a3cd49 --- /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 0000000000000000000000000000000000000000..1ad3f59d7c9b93101657ad1523a2939d02f504d8 --- /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 0000000000000000000000000000000000000000..c939095ad78a75eeb5f8b2e31f57e56b201b8a4c --- /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 0000000000000000000000000000000000000000..5c0d26a0583140b8ba0b47e937bc0dedc81e4fb5 --- /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 0000000000000000000000000000000000000000..7fdf6cc34e73e6543fa34e9b52e22382130d6f1a --- /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 0000000000000000000000000000000000000000..f729cb993babfa12140deabc9451eceee6b7885a --- /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 0000000000000000000000000000000000000000..bde739d75539be17496a8ce65b875b4f4b943940 --- /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 0000000000000000000000000000000000000000..37c270756ec4ec5a8a42b81b64bfbbe8e24f892a --- /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 0000000000000000000000000000000000000000..45d1a7ce415aa508a8a8f8d39f8032a22c2b4e5a --- /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 0000000000000000000000000000000000000000..6e035c21ed8fd3ad1eca7297921da359262e8445 --- /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 0000000000000000000000000000000000000000..31622bcf159a83dbf7dbc7960da3c490711a14ff --- /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 0000000000000000000000000000000000000000..2adbe0bc2868392e36079a5860ddf706543b210e --- /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 0000000000000000000000000000000000000000..d7cccf74c2e416dd8abcd45be121f73eccea3c12 --- /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 0000000000000000000000000000000000000000..04d8e51178a0a8ea38a5354aa421e20bd4091298 --- /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 0000000000000000000000000000000000000000..a184f4bc1aff9b3b212fc3cce7265cf58bba3948 --- /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 0000000000000000000000000000000000000000..defea0e1654f9267fa91a8b66e2bf1191b95aadd --- /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 0000000000000000000000000000000000000000..ff8b8c3b88b8885b52a090eeca3f21526bb0a456 --- /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 0000000000000000000000000000000000000000..969bc3b95c2194b922c1858ddf89b5d2461f11d3 --- /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 0000000000000000000000000000000000000000..75c55d8676eebdc09961d63b870e12fc0a91c5c5 --- /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 0000000000000000000000000000000000000000..a65e20b660481333e4e27e32203c9a5d12a5f150 --- /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 0000000000000000000000000000000000000000..53026b83d8b36505b4b0f32c6e6d4a4c690c7beb --- /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 0000000000000000000000000000000000000000..5766f46de868ad91fc0ff057691a7dea474a0dae --- /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 0000000000000000000000000000000000000000..19ee22eb511b987dd3acfc5c7c833d6561a4662d --- /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 0000000000000000000000000000000000000000..476a3baa18e42bb05edfd7ec0c3a2aef155cc003 --- /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 0000000000000000000000000000000000000000..d6bb3c7ef2f0e97b823bffb1d4ea1edd38609da9 --- /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 0000000000000000000000000000000000000000..175c1312fd37417cba0bbcd9230b4dffa24821e4 --- /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 0000000000000000000000000000000000000000..d7628202f29edf1642deb44bf93ff540aa728475 --- /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 0000000000000000000000000000000000000000..e3ebd73d9909a53e3fb721f2ea686f1dca0b477b --- /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 0000000000000000000000000000000000000000..0e8e796dab46c9de345166aa4dba818305b68857 --- /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 0000000000000000000000000000000000000000..40c4dd5398b454220d4d22dbbec08bcdb335be71 --- /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 0000000000000000000000000000000000000000..ba4a8f22fe574008e076c7983dfc5f743d03f2df --- /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 0000000000000000000000000000000000000000..5293fdc493f5577936977562c9457bbfa809f012 --- /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 0000000000000000000000000000000000000000..e18b32943be09aca0c53294e8e65187564ba1224 --- /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 0000000000000000000000000000000000000000..ac3b526fbbda03c5984d7c9dfaf937be520910a2 --- /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 0000000000000000000000000000000000000000..70fe381b68c5b95065275b5163af76dabaa5b22e --- /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 0000000000000000000000000000000000000000..4a3f549c2f6d7271e9a8fb225e18285d90312df8 --- /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 0000000000000000000000000000000000000000..2153b84428f354843aa7ffd3be174680440be90c --- /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 0000000000000000000000000000000000000000..aafcf555cb1ca06550c39419d20c257b02ea1934 --- /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 0000000000000000000000000000000000000000..e1607611b1a24957c7983041a540806b4275d289 --- /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 0000000000000000000000000000000000000000..ac128a2b083d6b94f17ee065d88226ff7dc53da3 --- /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 0000000000000000000000000000000000000000..e3e9f38af1fba0b278ed2c48bfc76cb2a6783307 --- /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 0000000000000000000000000000000000000000..df2091d1437ba51b4d1d6647dfa4d16ebd7dac53 --- /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 0000000000000000000000000000000000000000..942e8832eb4e99cd3af0dc61a1bde6ea01574cb8 --- /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 0000000000000000000000000000000000000000..048c40129134426ed628de6d386be9017b484d32 --- /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 0000000000000000000000000000000000000000..a622c3ee960ac4b61d03f4d7b755d98576e37b0d --- /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 0000000000000000000000000000000000000000..b8892d5d23632fd251938af55c0ae34a112ba058 --- /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 0000000000000000000000000000000000000000..1c2a4fd0e18cf47ee793eb6196f6b21e99bda6c0 --- /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 0000000000000000000000000000000000000000..8c165601a2a8af711ce771ea31b829405bccdfba --- /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 0000000000000000000000000000000000000000..bf2b504ecfcb378b1a93cf893b4eb070da9471fb --- /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 0000000000000000000000000000000000000000..26df9dbad8c28a6bd041e14bde9cb23624cf66ca --- /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 0000000000000000000000000000000000000000..15e395712342d3f4d5625d6159f3c1a5ba78e108 --- /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 0000000000000000000000000000000000000000..8fc1b84b825e7ed1d63ac0dee1b93c768ae42048 --- /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 0000000000000000000000000000000000000000..533ea6c678c2edb2355862ed4ab2712f2b338bab --- /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 0000000000000000000000000000000000000000..c41c4e01839621c0f3a3ec8c6a7c02d7345e97b2 --- /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 0000000000000000000000000000000000000000..58494bd416ab93113128a113c3dbaa5b5f268b2a --- /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 0000000000000000000000000000000000000000..0b3acbcebf2d7d71a23d9b89648df9ac532ae847 --- /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 0000000000000000000000000000000000000000..008457fff6861d102469ef46a234080e6fb0c634 --- /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 0000000000000000000000000000000000000000..62135d27edf689ce7a06092a95248ffeb67b8f9e --- /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 0000000000000000000000000000000000000000..4c557009286712b14e716f7e69309b0eb197d768 --- /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 0000000000000000000000000000000000000000..df3cb896c8f20de3614ce7adfd4a6774bead4ee5 --- /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 0000000000000000000000000000000000000000..05b22f8461a6d1a513b74aeb0ea976936e42f253 --- /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 0000000000000000000000000000000000000000..f5ee0a92af713fb3bd8c366f7400194d291ee7b5 --- /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 0000000000000000000000000000000000000000..33e9b65581b6a35b7f8c687f1b9dbab9edbb32cf --- /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 0000000000000000000000000000000000000000..b327158be4afc35744fe0c2e84b5f73662a93472 --- /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 0000000000000000000000000000000000000000..b0227e4ded7aef4a78baebcf10a511e0c5659f6c --- /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 0000000000000000000000000000000000000000..05939503b8a5c4caed24fe8ab938fbef8406ffdd --- /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 0000000000000000000000000000000000000000..d70daf7fed6ec8e6346e5800ef89249d7cf62984 --- /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 0000000000000000000000000000000000000000..38008c64e41e2addfea23f4c5f88bc04a2a49e86 --- /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 0000000000000000000000000000000000000000..83d249176fbf067a2732fa4379740cfa54bd018a --- /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 0000000000000000000000000000000000000000..ad252e64cf5c73d4cd6ae48dd0abede47d3323e6 --- /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 0000000000000000000000000000000000000000..767d1d289641510dca8f75431192786f294be2a1 --- /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 0000000000000000000000000000000000000000..6e19caab3504eef094cd4cffbe43b657dc1913ad --- /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 0000000000000000000000000000000000000000..50567c212eda4dca3f87df399dd0e6d0dc076c2b --- /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 0000000000000000000000000000000000000000..59e43649c93b1767739548a6bc8122886c6061ad --- /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 0000000000000000000000000000000000000000..95f5cc8f2f45dabe00fd376a8ac2db99155e686f --- /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 0000000000000000000000000000000000000000..0c1a17b3bd8a904d7274a18b5a4432681fb867ca --- /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 0000000000000000000000000000000000000000..0c1a17b3bd8a904d7274a18b5a4432681fb867ca --- /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 0000000000000000000000000000000000000000..c553a1422dbd52775efacadede6863d2dc0256c9 --- /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 0000000000000000000000000000000000000000..347d1ae13d84eaef1bf4ab33d65a9dfcf11292d5 --- /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 0000000000000000000000000000000000000000..5ca1a181e3887e4b5459c899aedb25acf60d4bed --- /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 0000000000000000000000000000000000000000..8b86db2f8a0baa6350a0ad772c083b22fd520be9 --- /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 0000000000000000000000000000000000000000..0ef30ff9554c558469c75252ef56a828cad2c777 --- /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 0000000000000000000000000000000000000000..9997fdc40398d3cf1c6ce30c78ae4d5b4f319457 --- /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 0000000000000000000000000000000000000000..8d571fcd66980e46d4e26eaf96870df6ff469408 --- /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 0000000000000000000000000000000000000000..49a05d5f47f7c07faa1403c5320268e6df2581a5 --- /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 0000000000000000000000000000000000000000..fc046bb9d9b03b5bdd5ea49dc1bedab8aacab656 --- /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 0000000000000000000000000000000000000000..aed0e702d7635421fc6674e2daafbccb0573314c --- /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 0000000000000000000000000000000000000000..c1bb138a244147fc61333952ee898979ce67351f --- /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 0000000000000000000000000000000000000000..8d48c7894afcb4949b1784f93c062014dcd207c6 --- /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 0000000000000000000000000000000000000000..e1894619c34441eb228587b9c50fc6af61193a44 --- /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 0000000000000000000000000000000000000000..e1894619c34441eb228587b9c50fc6af61193a44 --- /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 0000000000000000000000000000000000000000..5625e7588f1f33f057bf8ad15bc261c45072b1a9 --- /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 0000000000000000000000000000000000000000..c938052be8e21698e89e8a0f57215c71410492c9 --- /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 0000000000000000000000000000000000000000..d368a6d415fc340db7595a06b5686cbb920ad48a --- /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 0000000000000000000000000000000000000000..cc57d74ceae76b56074e8be073916301a280b9a2 --- /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 0000000000000000000000000000000000000000..4b5e0378401cd9f8530355d84da28d7ca507d0a2 --- /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 0000000000000000000000000000000000000000..df1e3fea9515984d0207b80e3ab03b39511d52db --- /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 0000000000000000000000000000000000000000..bb448d4d23511c57ab4216dd28af17232949c0b4 --- /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 0000000000000000000000000000000000000000..210d4c45c666164985e0f1998201d444c9a5f2a7 --- /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 0000000000000000000000000000000000000000..9b6439b043b367c5c300949f511ecb9866f2eaca --- /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 0000000000000000000000000000000000000000..2cc6ce120dc9af17a642ac3bf2f2451209cb5e5e --- /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 0000000000000000000000000000000000000000..0ee7420162a88fa92afc958ec9a61242a9a8640c --- /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 0000000000000000000000000000000000000000..f824fe55f9e2f45e7e12b77420eaeb24d6e9c913 --- /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 0000000000000000000000000000000000000000..d4beecd33a4a4a305407e1adfa2f4584c4359635 --- /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 0000000000000000000000000000000000000000..d39d26b2c1bbc40af8548cafe219f7cef2373373 --- /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 0000000000000000000000000000000000000000..81b14dd8d7932f9db417843798c726422890b32e --- /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 0000000000000000000000000000000000000000..6c19276554908b11c8742deb0ab4e971bf6856a7 --- /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 0000000000000000000000000000000000000000..bd1a841881c0cfa6a52364dfe57fd55e5a539fa0 --- /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 0000000000000000000000000000000000000000..2396e20015984b69e2c194c2c9e8552b1a2cc3b5 --- /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 0000000000000000000000000000000000000000..f341ab8abfdba5a9aaac3a81b709c75def92e46c --- /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 0000000000000000000000000000000000000000..5f256b42e1fde343b8194d199f52921e8ad01b7c --- /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 0000000000000000000000000000000000000000..7dc6caf3b052c956c9bb9ad4adc9ca245cfcf083 --- /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 0000000000000000000000000000000000000000..9134b9086dd5ddb9aa40a01875033392b2f92f89 --- /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 0000000000000000000000000000000000000000..6e6288d6bc3ffae240721c50c1a85c1a80270aa2 --- /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 0000000000000000000000000000000000000000..c595b790fc5065d5e4b276d4e73be1ccdeba7be2 --- /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 0000000000000000000000000000000000000000..e389a58d73bc4997d64b78426be26e964fd5b2b8 --- /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 0000000000000000000000000000000000000000..31c3730130fad5367eb87f1b5ce52b243ee4c1f5 --- /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 0000000000000000000000000000000000000000..ec61a48efabfc82a55a749860976dd694aee7a83 --- /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 0000000000000000000000000000000000000000..68be3b317b90d2fa990622857645bf21c1768c74 --- /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 0000000000000000000000000000000000000000..52f9e17312fb364b410edbcb21f3aa4b6f3c133c --- /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 0000000000000000000000000000000000000000..e46612cf566f59ffc8d8b8b6f4a8bcecd8779b12 --- /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 0000000000000000000000000000000000000000..4728b827df862d2e4db3363d9d518cebc860986a --- /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 0000000000000000000000000000000000000000..5d9af3357295415ea824128b9806d1ca895e8bb6 --- /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 0000000000000000000000000000000000000000..c3c91d211f47df42ad1c89911fc63e60499d3db6 --- /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 0000000000000000000000000000000000000000..9890e26bb815242173bf8a60a01194a9130a361f --- /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 0000000000000000000000000000000000000000..4db1d564cba5c32aae6260095811291c0614fdcf --- /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 0000000000000000000000000000000000000000..039168055b20d615f19400c4324857d0c038806e --- /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 0000000000000000000000000000000000000000..12afac8f5f9fdff743a7b628437ebfb4424fba2a --- /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 0000000000000000000000000000000000000000..0a5e2caaf1b10b271da7664dc3636528c6c00942 --- /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 0000000000000000000000000000000000000000..94928accd7e353b655baf5840ca2be8fb4afd49c --- /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 0000000000000000000000000000000000000000..91cbc450fd0418c590a1519da9834b6cdb72ff5e --- /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 0000000000000000000000000000000000000000..28424f4d51e008fafd30347e06e1deb8b3a6942f --- /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 0000000000000000000000000000000000000000..c2ed3fb1ed89ef2b9ba74e1c94ec778af5dbc7cd --- /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 0000000000000000000000000000000000000000..733bd791138444e03cb01f52b2e7428f93fbbc36 --- /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 0000000000000000000000000000000000000000..c154b4e86413741786fa3d608f6e466e91c01aab --- /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 0000000000000000000000000000000000000000..efde9ffa7e612179911c972a3c048fd389fe3276 --- /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 0000000000000000000000000000000000000000..0ff44752528fa0d4b31613a72446ed9164c419cb --- /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 0000000000000000000000000000000000000000..4ab1b260e3d35f9a6243e44ebf0f903add40b6b8 --- /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 0000000000000000000000000000000000000000..1056a7bffc268fef67c209f4c81f606d40fa66d6 --- /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 0000000000000000000000000000000000000000..4ed4605b2c60da836327a7064469425d5233858d --- /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 0000000000000000000000000000000000000000..5d7893796655c947c0e6bc0dba60c6e82c86bd65 --- /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 0000000000000000000000000000000000000000..7999a4ea468e87d9f0cd793e80c2a43454c4aeac --- /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 0000000000000000000000000000000000000000..bb52f493d75d79f91e3a6f34e103023e2cc8b87c --- /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 0000000000000000000000000000000000000000..648ca0b60324f4b92a617f377d890b8f1e1adf13 --- /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 0000000000000000000000000000000000000000..83df0a8959381ef48a3bd97b53f63f8d9a8bba0f --- /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 0000000000000000000000000000000000000000..fc6f86bfc2b48bdd4fb7acf8e9e08422fed2e91e --- /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 0000000000000000000000000000000000000000..1de0b3be6ce99de994a905cfbaf5e342754bb651 --- /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 0000000000000000000000000000000000000000..6aff5d6d446ea46b131bdea1efbd183bc0010381 --- /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 0000000000000000000000000000000000000000..821993fc70c13ebdb18a997d849db95424399d82 --- /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 0000000000000000000000000000000000000000..8e4a352f5022fe33402bd5267f32f925958a2a01 --- /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 0000000000000000000000000000000000000000..16617ecf7e052db05c5bccfe1da0bb378835f686 --- /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 0000000000000000000000000000000000000000..61aefb9aadd3c45a338e5c8048749d62c2c1bfe6 --- /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 0000000000000000000000000000000000000000..a021a2b9225d8eda5657a713b94f7145757206a3 --- /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 0000000000000000000000000000000000000000..ccd2be04e7757db3050e7675f093288b6d9a5748 --- /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 0000000000000000000000000000000000000000..073023e0f4df60364dede352b60fdc151e6f05d2 --- /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 0000000000000000000000000000000000000000..7c35df9cfea2b54cfffa84161902126234ba3234 --- /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 0000000000000000000000000000000000000000..1c302d1ffc1f1b7e1abb1f4a7553b69be224aac2 --- /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 0000000000000000000000000000000000000000..d5682b113ee37a34a42a65897f501af0ee04ffe3 --- /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 0000000000000000000000000000000000000000..be8370606e7fe33fd9eda9e440433236cc3f6d68 --- /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 0000000000000000000000000000000000000000..05ae93e47a4f16ce18cbe2ca3a709b3abc62d15b --- /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 0000000000000000000000000000000000000000..78f28f98e815d7fdd822d4a8710d686ad314ccdd --- /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 0000000000000000000000000000000000000000..6ea56a386fa133bf983a3a7f06b70bd12189e05d --- /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 0000000000000000000000000000000000000000..0b19d940d109bca37ade399a36b8b10c2812faf8 --- /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 0000000000000000000000000000000000000000..0f0138bdf6cade2297362c820831a995f7a4e02f --- /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 0000000000000000000000000000000000000000..2630b95ef029e231be2a854efa2cf4c50dbeeb95 --- /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 0000000000000000000000000000000000000000..3871b5d5ada8020c7d7f56510158bd89c4ab5ff2 --- /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 0000000000000000000000000000000000000000..8f6bfd5bbf82007be6d65ada0beb4914b450faf2 --- /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 0000000000000000000000000000000000000000..ce69f67f2920b6890eb4446dd6b260484e68178d --- /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 0000000000000000000000000000000000000000..bfa90ed0841f6ca8d26c1eef72e00d893d5efe0c --- /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 0000000000000000000000000000000000000000..bbc9826aa54afbf202153a170e642bb64f235298 --- /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 0000000000000000000000000000000000000000..a3a89bfa5059192bdb481a043cdde6d7e42c2f24 --- /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 0000000000000000000000000000000000000000..1a513b37d6846849b260a409b9993d3c708bfe30 --- /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 0000000000000000000000000000000000000000..5f847668e8986d4ba9be5cba4b6ddab65e61f0d2 --- /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 0000000000000000000000000000000000000000..684c1b934dce4e99b1485593bb8995576eae186b --- /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 0000000000000000000000000000000000000000..fe5f8201cc4da5e2cf6a1b770c538d421994e1c4 --- /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 0000000000000000000000000000000000000000..01f4c9bf669ba253edaa43dc641fdb9a1b7c51d1 --- /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 0000000000000000000000000000000000000000..2c6df4cebf1ea279fdc43598fff062ea5db72cb7 --- /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 0000000000000000000000000000000000000000..2b396029795aa7b9bcfb2f9dbb703cb491bf88f2 --- /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 0000000000000000000000000000000000000000..9412a018438b79130fbba167176860d2cef38106 --- /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 0000000000000000000000000000000000000000..f29ba25548a32eae3979249cd915f074444a0f51 --- /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 0000000000000000000000000000000000000000..dc85d3a9e7a3c3a5ba9d016bb88368b2b35cdcaa --- /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 0000000000000000000000000000000000000000..b62b6506ff4f7838e025ea98f93caac228fdd88e --- /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 0000000000000000000000000000000000000000..754b229db6b03264c0258553b18d5eea2473a316 --- /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 0000000000000000000000000000000000000000..1dac2ca2120eb3deebf39e9fdf8a353d14e0fb1e --- /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 0000000000000000000000000000000000000000..3d0a0e56c9063858f9d71c0cad7c43cdf448c84d --- /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 0000000000000000000000000000000000000000..c2df1526195d99956c0edb1e8c01a5ac641cbaca --- /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 0000000000000000000000000000000000000000..a2d01bff82923948a67ac243df677fc3d7331706 --- /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 0000000000000000000000000000000000000000..22baf82cff73662895c6aae20d426319b9ea32a4 --- /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 0000000000000000000000000000000000000000..b44e3d553c040d855204ac3d543cfa6539db7612 --- /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 0000000000000000000000000000000000000000..57262c76e6df8a60a11aa2dddde8a437824ef8e3 --- /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 0000000000000000000000000000000000000000..483a25a27ea1e7c94b21c91b15d929c5cd95ed81 --- /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 0000000000000000000000000000000000000000..37ad0703004b817ff2dd52dae5680dabdd5574db --- /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 0000000000000000000000000000000000000000..b399fb2f5a7ba00e088e9fc2ac10042452879e46 --- /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 0000000000000000000000000000000000000000..decf0122ef482aab10c213cad07a008e492b2e86 --- /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 0000000000000000000000000000000000000000..2559a393a9fc2368697619724887a7c7eb8b5a1e --- /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 0000000000000000000000000000000000000000..051abcc7033796d6ad5e65d2d0d5955b6bb51759 --- /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 0000000000000000000000000000000000000000..bb58e4727465e6c2cebb84e6c7a38b884b9ef13c --- /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 0000000000000000000000000000000000000000..6996765fd60b2e1c09182156b2ba8e19b3cca5f5 --- /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 0000000000000000000000000000000000000000..ad0842054f082e24c4ab145471c302d00cb9fea6 --- /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 0000000000000000000000000000000000000000..eb89b2912f0735b57f655fe08a33d6efdb5340de --- /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 0000000000000000000000000000000000000000..89a98bae4ea00e8562392aa2dda764d1d6203f40 --- /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 0000000000000000000000000000000000000000..edfeb64d5d87b0df6c25509d2077054613c4f543 --- /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 0000000000000000000000000000000000000000..92af9e1ae7f125fd9f36e1b67f43b9c71aa54296 --- /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 0000000000000000000000000000000000000000..808ddc4c2ce157903747ff88672425d9c39d5f71 --- /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 0000000000000000000000000000000000000000..57ce90219bc6f72d92e55011f6dcb9f20ba320eb --- /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 0000000000000000000000000000000000000000..be99968787df16246e5fb2bbeee617b27393496f --- /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 0000000000000000000000000000000000000000..577aae53496676a657164f0406c50e41566dae3a --- /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 0000000000000000000000000000000000000000..50205479c300e789b302cb1dcd687aaf7f9353f8 --- /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 0000000000000000000000000000000000000000..f0c3a60aee6f499a3dffd30d5d731612de3d90db --- /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 0000000000000000000000000000000000000000..e893cfa68510377d91301e796366babcc2cbb7aa --- /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 0000000000000000000000000000000000000000..cf1dfb7fa20bd8233e8ea75c51061b11f73302f5 --- /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 0000000000000000000000000000000000000000..f21a508514cac8c8da0626237726148ee8833953 --- /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 0000000000000000000000000000000000000000..79cdec2374c2e06a3f0afced560a27a9042cc63b --- /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 0000000000000000000000000000000000000000..2226aacb1a7e497f377fbbd607f125782b150f7e --- /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 0000000000000000000000000000000000000000..c43c90b90ba001df326c83df80b7d25152782cc3 --- /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 0000000000000000000000000000000000000000..e155bd59479ceaf31dcde7155b0503aa6f305a34 --- /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 0000000000000000000000000000000000000000..fb4ca0f9e3acb960fdeba9d86c973736eda25573 --- /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 0000000000000000000000000000000000000000..db6f1ef488b20f66fe89538461b72ba0b7827b54 --- /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 0000000000000000000000000000000000000000..7de70cc5dc1e852283f89a5048cc730e146ddd4a --- /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 0000000000000000000000000000000000000000..2893b347123f0a29be96b52ee0886ba716f365a0 --- /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 0000000000000000000000000000000000000000..1e939e2b2f31f4eef53496154dc4e7c086b28162 --- /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 0000000000000000000000000000000000000000..97f6f1120d988746a9ad95d33e8d24b237bec58b --- /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 0000000000000000000000000000000000000000..97f6f1120d988746a9ad95d33e8d24b237bec58b --- /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 0000000000000000000000000000000000000000..068dfd7bdfca25e8ac4834f7011e96b377a3ca49 --- /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 0000000000000000000000000000000000000000..4d073bf35f87e99198fc44258c8af746ff95e0b6 --- /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 0000000000000000000000000000000000000000..b02d3466adeb08e3ddbf4ecc3b6c554f1dd5872d --- /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 0000000000000000000000000000000000000000..dc7ea840878699d22280f6edf481b7c8ea51fa64 --- /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 0000000000000000000000000000000000000000..c991af6156cb38d143c574bcfb925364768c4f3f --- /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 0000000000000000000000000000000000000000..58328e4d1ee1091b8f909ecdfb22b836cb167a93 --- /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 0000000000000000000000000000000000000000..1302ea5fbe198800c08b2abc0cb79a2f4136d3b0 --- /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 0000000000000000000000000000000000000000..8670e1a04898e130c357c933f7edac966e2cfac9 --- /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 0000000000000000000000000000000000000000..dece8c51820fb451e57bf6efd313a00ce6050e22 --- /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 0000000000000000000000000000000000000000..6c54c4c8252e96ec9d762ffbbab596a72c163303 --- /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 0000000000000000000000000000000000000000..aa7dac30c1af6717056c15f4abafe2b3a1bb09ef --- /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 0000000000000000000000000000000000000000..66e0452bc0a0469ff6f7ff789f2db55a4fca4e17 --- /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 0000000000000000000000000000000000000000..a71638d52b0c90597a696e4671ce17f1c342681f --- /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 0000000000000000000000000000000000000000..bae247222fac0ed744593dcc97befe6051483101 --- /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 0000000000000000000000000000000000000000..70169492e4072dc561370d6185db255a229dd8e2 --- /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 0000000000000000000000000000000000000000..469b4c05d4eda8045d2534b0a5e8847d0b423851 --- /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 0000000000000000000000000000000000000000..68982ee5e92a85f193d80c6f7aa285722d1d78d8 --- /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 0000000000000000000000000000000000000000..0f1d1b4210991ec8d8718bef86c9959bec264c58 --- /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 0000000000000000000000000000000000000000..f60c35de7f3f5bb7eecf405c6370391aed6f3ae3 --- /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 0000000000000000000000000000000000000000..eb68cefcee916e168d25a58be9c4015fe131ecf4 --- /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 0000000000000000000000000000000000000000..82843f51c3b7c98cade0ed914ca18095e3d385fe --- /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 0000000000000000000000000000000000000000..92426ffb0d3aac123f647a9c3bcf07932de91407 --- /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 0000000000000000000000000000000000000000..2b17b7f5792c663e533d3fbe8def8ed44f12b7ff --- /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 0000000000000000000000000000000000000000..23f09ad7b271cb11e9660901a5d9d819a40ec9a5 --- /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 0000000000000000000000000000000000000000..319a1481f3e89c5c24535ecc03fffa89c83de737 --- /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 0000000000000000000000000000000000000000..57aac59cab28050f94d5cb93877e8d967f4661c5 --- /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 0000000000000000000000000000000000000000..ce5661e5cc9b983676fc97ae0d9c08e78878ee74 --- /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 0000000000000000000000000000000000000000..37977363b3046bc59bfd6eb74673a5a49d43d2f8 --- /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 0000000000000000000000000000000000000000..c4b1fe79ce21f963ad70a17278be8bef7804e43c --- /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 0000000000000000000000000000000000000000..b814ef420acc8a4a385eaf29d52a5a167171860f --- /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 0000000000000000000000000000000000000000..1807a51b4c60c764a6af190dbd957b6c2ebd0d91 --- /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 0000000000000000000000000000000000000000..6dea528ce9bd25a06962d5ecc64f1ca4b1c9d754 --- /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 0000000000000000000000000000000000000000..b529b2b08b42a17027566a47d20f8ae93d61ae35 --- /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 0000000000000000000000000000000000000000..8ff059b847b30b73fc31577d88a9a5bc639e6371 --- /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 0000000000000000000000000000000000000000..d67989e01fb7b70c728fdcf85360ac41ac8f2ff5 --- /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 0000000000000000000000000000000000000000..862a5aeb883e236e076caee3bec650d79b9b2cd4 --- /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 0000000000000000000000000000000000000000..673cf8cd0aa97a1ffd39409152efd6fe5cc1ef12 --- /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 0000000000000000000000000000000000000000..df877f95134803f7d07627ec1b22e6d076c6b595 --- /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 0000000000000000000000000000000000000000..b7a64fbd439720429ebe73c82340619e3d950391 --- /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 0000000000000000000000000000000000000000..e739d04efabdf1edada6c848c14c0e3ad3f62832 --- /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 0000000000000000000000000000000000000000..e7609908ff9436a9e9c4b366ad54b891c9868b64 --- /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 0000000000000000000000000000000000000000..21660fe7d307f5e78cf997778d0bc68f9a83f705 --- /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 0000000000000000000000000000000000000000..2a87f9372a66fd9e3b56807d0adde8fbb29a568c --- /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 0000000000000000000000000000000000000000..bd41d8ac191905eb40201c7779c247d86783bf67 --- /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 0000000000000000000000000000000000000000..32923bec58192f66bcce7f067208103d768f5a74 --- /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 0000000000000000000000000000000000000000..0cfd863903b3ae25d89ca93561d81ec245686913 --- /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 0000000000000000000000000000000000000000..a0cb605693638380d37ad3b6ff09c07d5b7cf3c4 --- /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 0000000000000000000000000000000000000000..68687be28da3d3500c2ca98113578f65b9465b44 --- /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 0000000000000000000000000000000000000000..20c52dff95ae423ef3decf9f88b6e13d7c42cbcc --- /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 0000000000000000000000000000000000000000..2bbbc82030a9ebe9b9871ec1cd18a572e688ef25 --- /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 0000000000000000000000000000000000000000..24fd40e36f3d1110f34a4ffb2cc5397f9aa6766a --- /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 0000000000000000000000000000000000000000..d99e7ab53f45d3e749b7d37d76829d8c083979cc --- /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 0000000000000000000000000000000000000000..e913ccc9a7a4297c47e82f978e5a4bda03d1f319 --- /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 0000000000000000000000000000000000000000..6559ef8c2b9e5ba003c6e3712f502a22416d6f04 --- /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 0000000000000000000000000000000000000000..18780e492c9a91b117148e72fd4fc0739f671d1e --- /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 0000000000000000000000000000000000000000..ebfd8f2a1236e39910eafb25a13e6466caa016db --- /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 0000000000000000000000000000000000000000..0014139716308461f550febfc71a83ec3f6506b3 --- /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 0000000000000000000000000000000000000000..aed1393b9c99cf654f3744bc92853c7b222725d4 --- /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 0000000000000000000000000000000000000000..5eb1b416d389bfcc405056f1e5da510cbe4aa272 --- /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 0000000000000000000000000000000000000000..7dcf2f58eb1d15dbe19a53626496a1ef7d87f975 --- /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 0000000000000000000000000000000000000000..334468509777c7ab550ea690cdc76f8627478e74 --- /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 0000000000000000000000000000000000000000..b529b2b08b42a17027566a47d20f8ae93d61ae35 --- /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 0000000000000000000000000000000000000000..a7f6bddb2e818210222895de24e072736eef14a2 --- /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 0000000000000000000000000000000000000000..a86c08be1a10c961aeb5a61412b891ad3bc9929d --- /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 0000000000000000000000000000000000000000..59dd7d9373ccdd355d3c6dc581bdfb18e6624072 --- /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 0000000000000000000000000000000000000000..5466fd9f0ebd8ffa94382d899bb250d2cb405872 --- /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 0000000000000000000000000000000000000000..e405396bef1c9898d024df78304034d0ad7d8212 --- /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 0000000000000000000000000000000000000000..04825a870bb77b3179e51e2b7fedd7a7197ba9e5 --- /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 0000000000000000000000000000000000000000..2ca7336b99efb11f67addcc31aca81f43f7078ae --- /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 0000000000000000000000000000000000000000..71b08a46d2917d9057d7331131ef6849f9335867 --- /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 0000000000000000000000000000000000000000..0c8f8164b4016a6724945cff0fb76700c2bea724 --- /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 0000000000000000000000000000000000000000..3ae2b56e3dbd78152ed91966b6b3a2474fc7c1e4 --- /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 0000000000000000000000000000000000000000..caac722ad07771ec72005752a124f1b86f080a70 --- /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 0000000000000000000000000000000000000000..62046a9e0f1f51239c1587aef16317d325ebef07 --- /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 0000000000000000000000000000000000000000..54d9e81224cbf55eca2a4f354f7fcfc8f98b6854 --- /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 25143914cc4828d14e36af9e4b77b5a41cc87d4b..af845ae4f2111c6009a6b0629777987b22f1b8e3 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 45e85fd04ff616054ac2a7d259c453d3ac92d76a..6fc06198fe8b24bace855b005d6d113f26555304 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 2d32b77d58a91b0a508a8da03754ce5eada9d64b..ca20802295923ec992ceaeb6dc2f514aa6a628c9 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 06be72742950a320ae483df70c37b2db8a4bcf02..591d6e443fec6e362a48dd40a6912a64f198732a 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 47c5f8c45855ca757a2b4e28503c8663a80b683c..afee6fcd2e998500ba55e08230473e91e68dbb96 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 8576c1ed6535303e187ae094a5ffa761fd80c4fd..9ae5c916b5cfb419e2a4e78d6641737fa55f0c75 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 0000000000000000000000000000000000000000..cf6e5ba4dfc482de58d05fddcd652da27eb2b623 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 0000000000000000000000000000000000000000..478b28204f24b76d1f43b647e4426f2a18e9a232 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 0000000000000000000000000000000000000000..69e8456f6c3eeb98009cfcade4efcff1d6940405 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 0000000000000000000000000000000000000000..7b72a90af16a0c3b539d0400fd175c4152369083 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 0000000000000000000000000000000000000000..1fe13e21b4246ed0fdf9d3778955b02b64468e99 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 0000000000000000000000000000000000000000..f8c90f691670f01f90505bb26592410dc7733d62 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 801c8f7172ec40d5c111109482d55e345d037592..8b46d7cfc58cd18d3b5b91919338e4049f882f5b 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 9d67cbd108e79145db2bae2c709ee4d7c0b61660..013565e14f449863f3b81be1d94bf4562b4323e0 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 40224b3229de1665e3fac89be0d035154e2cf67f..812fb051213f66fd8df1fb83d0b423b1f414effb 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 e5702cb677b62398c1bb75ae2054980d10c028f9..4d300230e16109558ac7f2d8850132912c76f67e 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( + 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 add_conversation( &mut self, - _pane: ViewHandle, - event: &pane::Event, + editor: ViewHandle, 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), - _ => {} - } - } + self.subscriptions + .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); - 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)); + let conversation = editor.read(cx).conversation.clone(); 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) - }); + .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 handle_assistant_editor_event( + 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: &AssistantEditorEvent, + _: 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?; - - 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); + let last_message_id = self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + }); - Some(()) - }); - } - } + 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; + }; - 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); - }); + 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() + }); + } - anyhow::Ok(()) + 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 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(); + 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(); + } + }); } - }); - } + } + })); } - }); + } - self.pending_completions.push(PendingCompletion { - id: post_inc(&mut self.completion_count), - _task: task, - }); - Some((assistant_message, user_message)) + 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(), + status, + }, + ); + 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( - message.id, + suffix.id, MessageMetadata { role, sent_at: Local::now(), - error: None, + status: MessageStatus::Done, }, ); - cx.emit(AssistantEvent::MessagesEdited); - Some(message) + + 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, 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); + + 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) }); - - if let Some(user_message) = user_message { - let cursor = user_message - .start - .to_offset(&self.assistant.read(cx).buffer.read(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); + }); + } + ConversationEvent::SummaryChanged => { + cx.emit(ConversationEditorEvent::TabContentChanged); + self.conversation.update(cx, |conversation, cx| { + conversation.save(None, self.fs.clone(), cx); + }); } - AssistantEvent::StreamedCompletion => { + 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()) + .as_ref() + .map(|summary| summary.text.clone()) + .unwrap_or_else(|| "New Conversation".into()) } -} -impl Entity for AssistantEditor { - type Event = AssistantEditorEvent; -} + fn render_current_model( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> impl Element { + enum Model {} -impl View for AssistantEditor { - fn ui_name() -> &'static str { - "AssistantEditor" + 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(&mut self, cx: &mut ViewContext) -> AnyElement { - 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 - }; + 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 0000000000000000000000000000000000000000..182e421eb88c96ac2792763c829d2bd02300843e --- /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 0000000000000000000000000000000000000000..b58e1f6aee89683a4b6300e107c66db016b882cb --- /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 0000000000000000000000000000000000000000..233b0f62aa5aa02d82e1524a2c7ea0fa96b836ce --- /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 6f31df614dbbd0f0761c93487d988ae9168f3d9d..cd2e53905d1ec58347b47e528178206f608e784a 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 906d70abb738e7267d35a59f501be96809fbf5b1..433dbed29b53d670f92e6cff0e222966c5bc85dd 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 4e83b552fb64fcd2b787eb80cb5ec2cd88cbafcc..61f35932479311de30df72ac0cc8ee17dcabcc38 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 4b9e7ba034fb23d118cb5963d335e5c16201d590..e7858869ce63906b75f9cd0cb117cf7b54283efd 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 775342359f4af8c68ea1caa1bf25affb31c4ba8d..87e6faf988d58290c8b0c659887e021cacd1abc2 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], +} + +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 ScreenTrack { +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 6a6a91b485520fd51fd7b427e920f9dc88bccbca..9c4e187dbc9fe8bc14bd6df8d0bf48850d238b12 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 - }; - - 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(); - } + 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); - 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 27545a652d3a4bea9b9bdec5b8ef8f6332c8bd4d..c61fdeebfb2f8452a47accedb9643af05bcb4853 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 595d841d075773699d88e4556354f4ef43b2b410..c690b6148ad120c41f9441bb95b5911b7e83e0ca 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 0000000000000000000000000000000000000000..e4348af0cc5c12a43fac3adecc106eb16a6de005 --- /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 7e2c376bc2c9eef6b030fdc7b2b4017d157f9216..208da22efe4b35564f756b8c481a31cf2047481b 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 f2df808ee3ca7d4792d649896b23c867e9fd444b..cf5090ab6d09ade92e0524770200724f5bdf2d7f 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 8d210513c2d3dc32fdac676b4953147cc4f0208e..14d785307d317ee03136f8cfbc728c73237d0451 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 b51c5240a833661152f4f6f9c72ae5f522e51344..b1d0bedb2cf23c856f320aacabb8d2fb489f2e2a 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 ce4ede8684831fd78d0d6a16cc49905581fadc1a..b20844a065133539ba71ced8a03217b262c4a69b 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 16cc6b52772e2772aab691f1e9e2b8dbf0e3b726..f81885c07a02fbc526d7c675762f3468db571d50 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 0000000000000000000000000000000000000000..16fefbd2ebc135a72e056a75e7a2486c7704cab8 --- /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 720a73f477e1c5ceb4e8888de95afefe937f1589..73450e7c7d9974f83d78d6c0ad7b4e26ec4fefa3 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()) - }); - - fn push_str(buffer: &mut String, index: &mut usize, str: &str) { - buffer.push_str(str); - *index += str.chars().count(); - } - - 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 project = self.project.read(cx); - let mut highlight = text_style.clone(); - highlight.color = theme.workspace.titlebar.highlight_color; + 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 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 c0734388b1512b2e9cac5014c460bf1a8c09650b..26d9c70a4378af9e8ff90fa4a0af8babf5a45539 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 b5f2416a5bc51e1d4bbe763082f77ff80b75922d..af59817ece40ab0fc3c15adb35c78b4d8aa84898 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 e8dae210c4c5ed196207fefb285c21c5a25bd1e1..428f2156d116133d063710fed99c890e8ad30869 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 abeb65b1dcc2d876e45dfa1d69fc3fb86b9d406d..cbd072fe8906dcfb49e8d7220b9b2d9657d4e894 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 2ee93a0734dd9b88aae34f07ebda21a8572af513..aec876bd78ade4312493c2853f51ac09f8dcb489 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 fb455fe1d0a76cfe2c0fdcc3fff0ec295fb89df8..f58afab361f4db153824ad9488fb295588dac425 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 e73424f0cd36945f1a24d3155521075c7967fa80..ce4938ed0d9e31c8130625b526095aeb0283e12c 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 0993a33e6c886323586c21620520440ca4bcd68c..803cb5cc85636d45946446b4e059a5e7d9aeae30 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 2454074d459b4938cbeebadb5cf7cf73589b5d99..5576451b1b0f1ac883ce0497f8df35a4fbcd245a 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 5350e53d6aef0cff11b8d7d96b148bcb3a701af3..d0cd437946a54cf6cb5446fecd0d310b1c82a269 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 f84846eae1e28d4a1dcd89172000983a6219f268..c106f042b5cf9328724eb72415cf8bea3fcceeff 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 a594af51a6307bbfda7b421c8a723e0c0e3563ef..6e04833f17ba1c4c0b706265540497cf2bc99824 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 05ff9886f1d33827776fa55795a4b0b6b26efd64..4b76ded3d50cc45d72385d70bbb424b139023f09 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 6ef1ebce1da844d1cd23185d5d246524716e082f..0b1523fe750326dea5c87f3ec4dcfa350f497185 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)>(); - 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)>(); + 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; - 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; - - 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); + edit.new.end = + InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize); + + 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 0000000000000000000000000000000000000000..6a59cecae8e2ecb172cd3e72b447b29cb33d42c4 --- /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 eac903d0af9c7e9c3d9a4bc184ce842e8d07cf52..0000000000000000000000000000000000000000 --- 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 d97ba4f40be2ebcf3e4d9e0aa5ad7c18d5772c37..ca73f6a1a7a7e5bff4d19a32db548c9d2155f744 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 478eaf4c7e1a16f56e55286b8e438cda9d78b4b1..f21c7151ad695b2567dc9cea7da5a5007be1696f 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 2e0d444e0473b4b4cfe2b79f712797c2e8e799f5..e979bd9c1edef14fdc73500c760d481c2981d403 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 e2b876f4b763916f855c6d501bec50109b9cc8c0..9e726d6cc45437bbaf287a1e82849ad66a60b9a9 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 5f7843f721dc133cc1fb234a88bbcb1e8b7cb705..0a462b1e5d39489b6abf10d4cca380fc966d7d35 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 0000000000000000000000000000000000000000..a6ea3962d28f27ff45cab2efc5f85a4a3a861d68 --- /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 955902da1263f875c6e3c3779b6cf39d84ccc191..31af03f768ea549d04a8802623bbc364acd762a4 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 9a5145c244880e37cd27992886d7a4740f5b902b..1be4dc2dfb8512350f841ef44dc876c890c3c185 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 a13619a82a0abfc82bdc3172b42a61bb5030d252..d595337428dfce0a8f8fa9d5ad6c001b8072ba3f 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 da5e3603e7e2326c690ba3e3a80213874cf325b6..82c2e10589a4d15e8f1c1fb0ac158764878255f1 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 6f6c21f0d4d132d5d536beeef5fd9e1259906641..f9d09adcf50bfbadb67bd379a888aaaff7ce3bce 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 d32a3e5b4c863be333ca3077d0988bdbd34dce9c..beb5284031fc82e157b06240b7dadc04be08be93 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 56bc235570742d90850886b2460322035fa3c2fa..15f77bd561eb900bbdeb213f4fb37adc3be22697 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 73e7ca6eaa1cf56f98115a6607632f10da422fd4..3f6bd83760f855c1b5dd0d3225eec6b9b8ecfa98 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 7dda3f7273ee8513b51119513d6d7e2a3b9bf4f9..b3ebd224b08a21d47f5636b6fb1da8357b65121c 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 fee7765d497c865903bebe10cdb4a776e565559a..ec8a249ff4c4626e420f072a148b268831e61ba7 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 488262887fd6c5c47ceee129840d72a5201531ca..0e5fd8343fa1a7f441e9d7474c4ea0a96c0df575 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 0b41ee6dca3630450668a7eac69aeea2e99d3400..769f2eda55b4a165e138ee094adc4a581963a641 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 882800f128abe8d41dd48e83a694b48d0020778b..640614324f65dcdcae29c711a698713b48cfd9d0 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 5b4df68a651cfa63c9a1b1976a076994d9971747..c6b43e489ba55e2fa259b9d3a2d1fc8ed9c2a564 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 cfcef626dfa2a5baf36a230eebcd1e5ae2d23495..58d7bb4c4035f03083482d4a83e3c5b7b6e855f1 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 b6c1e3aff97127fd7e0c3231ae7b25afe87f7948..2a0c2c1dc1ae14d430fb3dbea744f8fd6fb19271 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 779f4b6ec3d3d7581e26ed1c1ffa8c21382ce9f8..78403444fff1807ee7b01ef8fe6a8f8493edfab2 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 3ce323db09775086ba1eb0897d71c6a51a0dd9ac..3b95feb9ef86b7c49f6535fff38bfcc4183888cb 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 98c5ae6226940946542313021e658243c843b6ea..df200eae7ff256424ffc9b402cc87fb6ba72b3a4 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 9499841b3d46a0d8c77da4e741bd1c623eeb0de4..d9cf537333c4e60b81f1cd218f678eae4f0a19ad 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 1cf8cc986f08afe5325b3fe8f756192969487e99..4c6298d8f55a28abeb6cec468af63a94eec21759 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 6f2762db66144f1099bd05cee01be43f399b0141..1b8142d96464ff7c9f30230510a13c58c6f08cb6 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 544422132298be738e97a29a0cd9f7e4a56eba38..9792f16cbe19b65c7ff8d7d620f71d87d3c51f57 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 7b4892fc1c2f37012f0124113c5888c964d0232e..f21b1c363c0d55403654b92394d24649079cbb34 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 365766fb9dd0b080642a4b1e344d985dd312d22c..712c8544884af838a6d5cccdc5f6555a77b8014f 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 57dad48e341005ff267dabe2af41951b1d41d657..4f0d4fd46159aa259d94f404e78bd9d5a55c0365 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 5e77593d05298b232f83b0f53d0c947e79501b67..3b4a94dd0e69137ab95457d3991f88e245758243 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 a172667fb99ca53f077285d06a4ba12a5c6f0010..3442934b3a9520ecca113433f420c63a15140489 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 7fc02b054808786d412b45d532d72156c1ffefa5..67f8e52c04f0cdf18aaf340860d0fe9adc44bc88 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 8b5b801adaff3bd337754ba8e59bf6d12db2e7d3..e1d80fe25ceead41828aa18d90f5938eb518a17f 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 96edee1757af70db22df56ebe12f979ec9a58860..7793a28ee0d9dea61ae1f46f6df475b5a8824ce6 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 cf0a08f33ed50ead5e5f4619cbb024603de780a9..a492da771b848574e29411e91d8876873ead4917 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 0efc794148868c537eae37f4027ae8f23afa177c..ca2cc04b9d1cf75c54ee9593705172f93c6f8ec4 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 e976245e06b3bc29e904d6e02db35f476498e1e0..dbf57b83e5bececd63fd7c5963e8ea0514b81708 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 e91d5770cfba80485f3cbe6272257a81c48c4fc3..e8450344b8a627994cbe173a52c900339aa93cf3 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, 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.cached_server_binary(container_dir).await + 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 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 task = { + let container_dir = container_dir.clone(); + cx.spawn(|mut cx| async move { + login_shell_env_loaded.await; - let server = lsp::LanguageServer::new( - server_id, - &binary.path, - &binary.arguments, - &root_path, - adapter.code_action_kinds(), - cx, - )?; - - Ok(server) - }); + 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 binary = match entry.clone().await.log_err() { + Some(binary) => binary, + None => return Ok(None), + }; + + if let Some(task) = adapter.will_start_server(&delegate, &mut cx) { + if task.await.log_err().is_none() { + return Ok(None); + } + } + + Ok(Some(lsp::LanguageServer::new( + server_id, + binary, + &root_path, + adapter.code_action_kinds(), + cx, + )?)) + }) + }; - Some(PendingLanguageServer { server_id, task }) + 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 832bb59222fc79dfd00979c0b15306c3d18a3d75..820217567a60b3ce7250014b85fd104967d762a7 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 7e5664c1bd68d85fa83ebac497acb15e43d0eed4..1570baf1854154a0f40a2f3ce137a291dede5b99 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(); - - // 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; - } - } - - 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, - }; + let next_new_range = new_ranges.peek(); + let next_removed_range = removed_ranges.peek(); - // 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; + 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 { - break; + 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()), + ), + }; - if end_ix > start_ix { - ranges.splice(start_ix..end_ix, []); + 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; } - 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 57b5cd4a8c8de6a60f46754034edd6ca6ed238fe..272501f2d08a1cafe0957a67c31f57e19d63204b 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 2c78b89f31c61fd7193aaf15eb1b90c15b39fdd4..b97417580fb645b8051d1f3cb030bf914c0ca067 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 817901cd3a6a3f480a19ba5fb34d11e6c20074ae..6362b8247d39b3d6dd86f339d5f72b56c94da984 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 04f47885c0e084a9eb86cb529896624f34c8d536..12d8c6b34d3abd93e25e1b18e03656de477d417a 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 075df766532a634d06e9c558f86009516ae9d435..3e6727bbf4794eba1dc81d363c2b56a23c5e6a95 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 a0326b24a1c480d55bc307b178f6562cba1b3d07..40d3641db23afcbac801b7f5f2daa15411c15f72 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 96480e92bc53095e68d80c5d86e6b12c5e7de15b..f5f6d0e46f123d8015f7674d8c0292f1f35d3d75 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,35 +59,107 @@ 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(); + + 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"); + } else { + panic!("unexpected message"); + } + + 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 track_a = LocalVideoTrack::screen_share_for_display(&display); - let track_a_publication = room_a.publish_video_track(&track_a).await.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) = track_changes.next().await.unwrap() { - let remote_tracks = room_b.remote_video_tracks("test-participant-1"); - assert_eq!(remote_tracks.len(), 1); - assert_eq!(remote_tracks[0].publisher_id(), "test-participant-1"); + 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_track = room_b + 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 2ded57082801428e35ff5ce87d1b8740db0fffb7..0467018c009a9b36f00908d6628311d580fdec91 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 f45667e3c3e4b4d00f264b51ff4782e8471e1ebd..6daa0601ca95541665dd8bd9c2eeaf526320df8c 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 8d1e4fa16abca12af30878779b7cf7d403f74250..3fc046c5a293b105729fb756adfede6951de954b 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, _publication: LocalTrackPublication) {} - pub fn unpublish_track(&self, _: 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 96d43820758ee0ad17dd8bb11861fc57573f3f17..a01f6e8a49ea744cd4c1d699ada7c27474490907 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 fce0fdfe506201e6d126c141c0b692bd7ff39ada..53635f2725ba7a982d773abfd642104bca57d10f 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 e2a8d0d0032e23ea6751855acce33d90bded46d8..27a763e7f8851722907749c1a28a958827c3f9f5 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?; + 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" + )); + } + } - 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) - )); + 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) + )); + } } - Ok(()) + 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 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")?; - - 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) - )); - } + let packages: Vec<_> = packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")) + .collect(); + + 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", + ]); + + 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 1e364f5fc8f6d9198fba71b6d11429e11e13d3e0..f93fa100524b8371238dbe880f9519205c9c82cf 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 69f16e494933ed0a47bc8bf45c08e046b3e55372..d09de5320ceb8b4482f0e83c2deee0cc5b0397b3 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 d6578c87ba6232461750de71091383618164f48e..bfe5f89f682c9f3a5333ae007366339d4c6b1577 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 8435de71e2f420e479035a5d21b36e850db29834..eec64beb5a3f25300d3147cde2033a58c235d53d 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 5069c805b729698b95a0988f56d15417cb2cc0c6..bbb2064da2c2d0a877168ed39d0fe2ad848eba91 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, - initialization_options: Option, - pending_server: PendingLanguageServer, + worktree_id: WorktreeId, + worktree_path: Arc, adapter: Arc, language: Arc, - key: (WorktreeId, LanguageServerName), - cx: &mut ModelContext, - ) -> LanguageServerState { + 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 languages = self.languages.clone(); + 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(); - 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()?; + 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); - 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(); + if let Some(container_dir) = container_dir { + let installation_test_binary = adapter + .installation_test_binary(container_dir.to_path_buf()) + .await; - 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( + this.update(&mut cx, |_, cx| { + Self::check_errored_server( + language, + adapter, server_id, - params, - &adapter.disk_based_diagnostic_sources, + installation_test_binary, 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()) } + + None } - }) - .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(); + self.language_servers.insert(server_id, state); + self.language_server_ids.insert(key, server_id); + } - language_server - .on_request::({ - let adapter = adapter.clone(); - move |params, cx| { - Self::on_lsp_workspace_edit(this, params, server_id, adapter.clone(), cx) - } - }) - .detach(); + 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, + }; - let disk_based_diagnostics_progress_token = - adapter.disk_based_diagnostics_progress_token.clone(); + 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); + } + } - language_server - .on_notification::({ - move |params, mut cx| { + 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 AsyncAppContext, + ) -> Result>> { + let setup = Self::setup_pending_language_server( + this, + initialization_options, + pending_server, + adapter.clone(), + languages, + server_id, + cx, + ); + + let language_server = match setup.await? { + Some(language_server) => language_server, + None => return Ok(None), + }; + + let this = match this.upgrade(cx) { + Some(this) => this, + None => return Err(anyhow!("failed to upgrade project handle")), + }; + + this.update(cx, |this, cx| { + this.insert_newly_running_language_server( + language, + adapter, + language_server.clone(), + server_id, + key, + cx, + ) + })?; + + Ok(Some(language_server)) + } + + 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_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(); + + 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.on_lsp_progress( - params, + this.update_diagnostics( server_id, - disk_based_diagnostics_progress_token.clone(), + 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); + }); + } + } } - }) - .detach(); + Ok(()) + } + }) + .detach(); - let language_server = language_server - .initialize(initialization_options) - .await - .log_err()?; - language_server - .notify::( - lsp::DidChangeConfigurationParams { - settings: workspace_config, - }, - ) - .ok(); + 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 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; + 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(), + }, + ); - // 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)); - 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(), + }), + })?; + } - 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 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; } - // 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, - }; + let file = match file.as_local() { + Some(file) => file, + None => continue, + }; - if file.worktree.read(cx).id() != key.0 - || !language.lsp_adapters().iter().any(|a| a.name == key.1) - { - 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 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(), + ), + }, + )?; - 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(); + buffer_handle.update(cx, |buffer, cx| { + buffer.set_completion_triggers( 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, - ) - }); - } - } + .capabilities() + .completion_provider + .as_ref() + .and_then(|provider| provider.trigger_characters.clone()) + .unwrap_or_default(), + cx, + ) + }); + } + } - cx.notify(); - Some(language_server) - }) - })) + 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 3c23c30ab973ad76c003e467503ed5dae47f1d65..16e706a77eeb9b4a92a368d7bb653639d24e9814 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/src/d.txt".as_ref(), Default::default()) + .await + .unwrap(); + fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default()) .await .unwrap(); - fs.create_file("/the-root/d.txt".as_ref(), Default::default()) + fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default()) .await .unwrap(); - fs.remove_file("/the-root/b.rs".as_ref(), Default::default()) + 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 561da2c292842e83fc9f10be8a90db21bc6349f2..2c3c9d53047e48b55f556038504bf3546a4ad284 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,54 +3139,155 @@ 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; + self.send_status_update(false, None); + } - 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; + 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(); } - self.send_status_update(false, None); + mem::take(&mut self.state.lock().paths_to_scan).len() > 0 } async fn scan_dirs( @@ -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, - ) - } - } + 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); - for new_job in new_jobs { - if let Some(new_job) = new_job { - job.scan_queue.send(new_job).await.unwrap(); + 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 3abf660282a8663626fffb1bfbb9db7fe2147a72..6f5b3635096e334b57357f633370782f6a2a965a 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,40 +263,506 @@ 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": "", + }, + }, + "dir2": { + "src": { + "c.rs": "", + "d.rs": "", + } }, - "ignored-dir": { - "ignored-file1": "" + "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; - tree.flush_fs_events(cx).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(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .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,39 +1767,37 @@ 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)); - let tree = Worktree::local( - client, - root.path(), - true, - Arc::new(RealFs), - Default::default(), - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - const A_TXT: &'static str = "a.txt"; const B_TXT: &'static str = "b.txt"; const E_TXT: &'static str = "c/d/e.txt"; const F_TXT: &'static str = "f.txt"; const DOTGITIGNORE: &'static str = ".gitignore"; const BUILD_FILE: &'static str = "target/build_file"; - let project_path: &Path = &Path::new("project"); + let project_path = Path::new("project"); + // Set up git repository before creating the worktree. let work_dir = root.path().join("project"); let mut repo = git_init(work_dir.as_path()); repo.add_ignore_rule(IGNORE_RULE).unwrap(); - git_add(Path::new(A_TXT), &repo); - git_add(Path::new(E_TXT), &repo); - git_add(Path::new(DOTGITIGNORE), &repo); + git_add(A_TXT, &repo); + git_add(E_TXT, &repo); + git_add(DOTGITIGNORE, &repo); git_commit("Initial commit", &repo); + let tree = Worktree::local( + build_client(cx), + root.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + tree.flush_fs_events(cx).await; + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; deterministic.run_until_parked(); // Check that the right git state is observed on startup @@ -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 55efc09deb179437d837e7f50e1acbdb6613a2eb..33606fccc41854e3ae767fc691cd0edf15f047e2 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 9563d54be8b05233ed3df121efb2de9a37b0d5b7..c329ae4e51d0477eaba115c72597cd6248b6432c 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); - } - Err(ix) => { - expanded_dir_ids.insert(ix, entry_id); + 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); + } } - } + }); 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 0dc5dad3bf8d43edf3cb4c18507a7389ca65c2e1..fc17b57c6dbf92eee74874f9cb1577f495ba8704 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 14f8853c9c0aa9177365bb49b3f366d8fc3f2c0d..51774e8febaed924f201a8f088c86bd3ec0c0e31 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 a1dc8982c79fcedd36447f5bf116f068701b238f..4ba6103167b29669376cb69354b9a942feb96196 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()) +} + +pub type RecentProjects = Picker; -struct RecentProjectsDelegate { +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 6b6f364fdb0b7395be9db6d516a46f1b40d042bd..2bfb090bb204fce393b7ac816e1000413228c056 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 ce4dd7f7cf5514fa56aa4a62f1ed4553a9273b54..a0b98372b10f05248e1c49fc14844b9de61274f9 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 13794ea64dad1446e579dd00806ccb4afda4758b..605b05a56285a2f2c18c001136898adeb1e75d97 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 8b101670918ad42dc58e9039f8150fb005a1595b..6b430d90e46072a6f885b60a4e912978ed26c6a2 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 87a8b265fb2f774135e5db267a269c3890eaae21..59d25c2659f5ebbcad2b0a7ca4825c8a5bbf0d37 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 27aac1762bec9f37389e0cba1a47d4e5a11016e0..135194df6a2f5a22c1ed001a725488ae2c003437 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 dab4b919923678bf73ec588e5affb7cf48d8bda4..06b81a0c61139ce0bd0a0c58a6101b8a043393bb 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 e607a254bd70d066be2cae4efcb78d82d222a861..93cb2ab3d74bd873f55c75d4b4415e7fbf782b51 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 39c6a2c122e567155d76245d883e2fc6e4ab205a..1188018cd892143788c0f6629aa72425eee2dc85 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 88412f60598e8e5eaafbb52515089b580e2613a2..59165283f6642fc2ea097113a61255cc98a6c25b 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 6c6150aa3ab45f9b7467a30ea060fbfb0fb27e69..8d219ca02110827d4c76fe170f6c541651148170 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 ea69fb0dca9bfbd5a87bdd22d86c1c22c22af2ca..4bb98d2ac8668cb96afbcf31c717f4bb87dbe16f 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 2f2ff2cdc3bde8bb5ee1c4a32978ed28518bd94f..b92059f5d605bc16c6578b254ce0431113cce200 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 ac3875af9e10704be06a7a2a047f86ec1fe992b6..11f8f7abde0509d970ebd65a972246e40ba2d05d 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 2693add8ed0ac0d9d032ad5bd3f5e540138cf2fe..7c94f25e1eb73c125e61effef56565cb9597bd85 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 95d6348c626aafc28a032716eede382b62e0019f..1949a5d9bb309767a52286aacf28cba03840556d 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(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)] +#[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, JsonSchema)] +pub struct SavedConversation { + pub container: Interactive, + pub saved_at: ContainedText, + pub title: ContainedText, } -#[derive(Clone, Deserialize, Default)] +#[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 f86d3fd8dd38477993f490d1824aab6ee232d646..b9e6f7a133a42d05d99aec6ab76af395554c160b 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 b86bfca8c42ae05900d76d86e19544531c245899..308ea6f2d7b5e02c222a4e9f049ce58fa866e2bd 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 a6c84d1d91a6b149b3ed4950c2127f194040fc4a..5775f1b3e75fe3f80e5b0dd488b95f57724364cb 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 32dca6a07ade6123617ae3bfa480a6c231f9cecb..0000000000000000000000000000000000000000 --- 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 258249b59932c8c087d5e27a85cb75dea7dbf2f2..0000000000000000000000000000000000000000 --- 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 e3397a1557cfeed520e4c56fa38c40e4539d71a9..7ef55a991822a52ba18e3dc45184f9881a38dc74 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 71ffacebf34e2474bcb1c394dada124827c53477..c8beb86aeff44a75482531d22a6e8b0c07155fa6 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 cc3ec06c4779c143fa4d9eb3e62d4a1090e430d5..faf69d94734f95cd79dabaef3a165bbec7721c81 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 1f90d259d3e73801af58621ee4e80d925647e489..1227afbb85e4f8d78b5dab54d9b9f5410f588863 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 0000000000000000000000000000000000000000..ba527af0bb0b994f571dad2cf5a5ed466a9e3b98 --- /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 350ffdbb8f8b427447be5ed03ce2e347c0e65dad..d226c704108b6bd036c08f1700cbb7812a3c1c51 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 cbea65ddaf7334f753187cc36fa2e1d5b04b2ed1..56fef78e1da3ce837fa6b5d9ae2a5e23fc4f3f26 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 0000000000000000000000000000000000000000..7b068cd7936d5215bcca75187f853ff5e933f8b7 --- /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 0000000000000000000000000000000000000000..ef72baae31d2bced3bc676c7a52e1396be0f5fb1 --- /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 aeef3331279c497743caff9ede882f341b221c01..7212a865bd56a8eea01db7efd020d714b26a17ee 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 0214806e1195bd5d18f97f6eb15de8e023bb17aa..b6c5b7ca51c928072c242ee534bc29c29833878b 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 d10ec5e824b7a720b8136a4b3d694d3c42d0b62e..eae8643cf36e7897a0ed995c41d0bd4177d889f8 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 c3728db22271ca4884c7d50addfc9d7cbfd56e1c..5e22e77bf08eca70ac71e1ecc9202e5d6d2d325c 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 e44b391d84b3f46d96e84c8e97589b6ecbde699e..cf24a9127e980b21241ab16e20119041b7447a9c 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 48f486381dd223340becb30eb1c594e5a1f55bb4..ebaf399e22b3ecfa51b217327f5912a34dc7b542 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 9a3fb5e475dd0c612fecccfefdecab95606bb776..a3e3ab92999fe42e3f8e6a29ae82745883c26df2 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 1e3c6044a1651101939453d2ee2e5c6b90df3564..09cfb4d5d805561bd4b7593fc08355bd64badf9e 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 551bc831d3cc074a95cef62479b26407c6fe65bf..6a20fab9a275571bd52a55b804e7d6b6407016e6 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 d27818d2028eec866d17cc687bb393f37e9cb6e5..dd2aa5a818968c77e94f8ccb1ad0bcaf284f0ecb 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 9e3c4012cdcb77e082befb553488512f121569af..10750618530642465d73eb3d4016e1d42095c85a 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 8b26b1181bca243d38cc915455cbe0df9403de5b..69394b842132b61b6403537d76eacf3f6b0b484f 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 ef8cde78a64aa1260d6be8174b24eb418762ed4d..60aefe42134961ef31972dc2c75d19d2815b9514 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 0000000000000000000000000000000000000000..3d3f7d4357474cc64018eb3876d281c5cbe56460 --- /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 0000000000000000000000000000000000000000..bffda1bc1679cb24a85a4e3840fa5d4a19d9ba48 --- /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 0000000000000000000000000000000000000000..38e5658ce7ed1f3ead56253e6913f87ca183cabd --- /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 0000000000000000000000000000000000000000..b3fe3cbb53c46457c6855a916dcc6ef650e6be50 --- /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 0000000000000000000000000000000000000000..bcd086924d2d0b36000bd4b67d2567dbc19c589d --- /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 d8e47d1c3ea6a0ce2fe099a11a37ee53d66158b2..d016525af40a9e247517dc1aebd64fa2c99c703c 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 6eb8a44f0fb613a637af1fb6e20d2a80a359bf9f..574016c25d00e75e6fc1bb57e4b5fbb57f92e07b 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 7e4ddcef19189cb0a5704d43ca78a6164186d1de..47aa2b739c3773fe701552a0ac17477c15ee963b 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 2939a0fa5f942b9631b2ecd9e13d6c1e5c4de17e..c32927e15cfc177fe8c4611f8b5de686ebaffed6 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 deea51c436386eb36b1ed41d61cb5d21a787ad20..0e779d195c5e6e03404c783d9675fc223232c84d 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 ed24abb45c40b6b2cc2c2336a746557c03505745..d7982f7bdb471d6b0a154d60094f6e206618f0b1 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 fa88acd4d99239f97d4f8a2b26c098e0322d49bc..8728110d5826959b893b352dffd8c8e01a4d29ac 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 0d4977b28a4749de6851120e60bceb450d526bf1..b503bcb28dc10911b9e57e74f7217a21ece549fb 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 68f780c3af73b3febd1fdcbeaba1a2a95bb36b37..ecc839fca6875e54f0a0bf4d0671c675123418ef 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 e1f3da9e0238f0a91179447e7c15ce2df5239ec1..b7e4ab4ba7b32491bbbb8aa025cab543dde113af 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 9b82713d082d5d12a0243ebaf674b9a16550c3d4..b0719363929232aa47721624a63a9b8bb4aacb5d 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 f204eb2555f5327d0e70df60963db771acd772ab..7c5c7179d019ee9c8c90e069fec55f297ceda2d5 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 7aaddf5fe8a72d5731322e911b5a5c87473dfdbe..41ad28ba862e38e04b52ec5e4e1b77e87b183200 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 d387f815f0cd345c4173f522370ab17495989592..358441352a7090084c46e2f6fd1d06b2aed8c68c 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 15700ec80a1bc798d44210ed47d600cc319a1241..97549b00583d45f3b03a730d2a10f43dc9b2f978 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 7d2d580857780a714496a5f8c2c850de36986d26..0a47d365b598aa41df1c1fa50aedd7d718aceb87 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 7f87a7caedb764588a698b5e863fbd418c3859ed..b57c6f5699498706de0b1c91655fb15848b1a2c6 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 73a3346a9adc75b61af3ab4e5f50b4484129bb1b..3da8c24617909abb00cd6aa19e1d6adff6cad010 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 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()); + 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 + }); + + 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 088d26be78ad3048fba5516ac3f67eb3f846d600..0df16f4bab13fa56ed3211e462d8203be161c331 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 0000000000000000000000000000000000000000..59100d3628e6a1e1202c1a4759c2f6a3d594e5fd --- /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 0000000000000000000000000000000000000000..78653571adfccdb1fca8cefe9f6408ddf995ea56 --- /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 0000000000000000000000000000000000000000..389d0774a78ab162e46d938c952ff071c3874334 --- /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 0000000000000000000000000000000000000000..70e545981145726086c0926c82c819db88c15e1f --- /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 0000000000000000000000000000000000000000..0fc5bfbc893209dba2c72bcbd1548598eeb957b5 --- /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 0000000000000000000000000000000000000000..7d8054af673b5fb38c68bfff0c04a08ded7c2f0a --- /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 0000000000000000000000000000000000000000..ce43d647bd723d1343d00d3f1c571c29e2df6092 --- /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 0000000000000000000000000000000000000000..6e424a6f8145bc26235cc4e6eeca2db1d52df234 --- /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 0000000000000000000000000000000000000000..d4331ee367934453b19c6cd30af57924409b34d7 --- /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 0000000000000000000000000000000000000000..b78631f3d16bbedfa3f62aa4129440f7b945844b --- /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 b8632c4c229d754fec6ce8c19a4c892eea115c8a..b702fb4e02f9d0e3ae2a70ca99054b7bea2a711b 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 0000000000000000000000000000000000000000..485ff73d10441b93e118bbf1a6a89ba5a5f3a8d1 --- /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 c2658d7d1b31848c3b71960543cb0368e56cd4c7..25fbf5a1c42c82c0f45aa74514722b67863ba17d 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 0000000000000000000000000000000000000000..b83ccdda6a71debf8e212f52dd8cc288dd5b521b --- /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 0000000000000000000000000000000000000000..5c31fc5ac13790f1fc813811dce63904b9a79b6a --- /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 d1d0ed0eb8f810cea84e6efbc3e7aa7aa6e3f899..6fc5f746e52e93f16263d37f6d1ef57cebbf822f 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,344 +318,4055 @@ "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/acorn": { - "version": "8.8.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "bin": { - "acorn": "bin/acorn" + "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": ">=0.4.0" + "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/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==", + "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": ">=0.4.0" + "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/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" - }, - "node_modules/ayu": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ayu/-/ayu-8.0.1.tgz", - "integrity": "sha512-yuPZ2kZYQoYaPRQ/78F9rXDVx1rVGCJ1neBYithBoSprD6zPdIJdAKizUXG0jtTBu7nTFyAnVFFYuLnCS3cpDw==", + "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": { - "@types/chroma-js": "^2.0.0", - "chroma-js": "^2.1.0", - "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/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==", + "@typescript-eslint/types": "5.60.1", + "@typescript-eslint/visitor-keys": "5.60.1" + }, "engines": { - "node": ">=12.13" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/mesqueeb" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/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==" - }, - "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/deepmerge": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", - "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "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": ">=0.10.0" + "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/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "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": ">=0.3.1" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "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/nonenumerable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz", - "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q==" - }, - "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-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "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": { - "@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-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" }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { + "typescript": { "optional": true } } }, - "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, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "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": ">=4.2.0" + "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/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==" - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "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": ">=6" + "node": ">=8.0.0" } - } - }, - "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" - } - }, - "@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/@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" } }, - "@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==" + "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" + } }, - "@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==" + "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" + } }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + "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" + } }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + "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" + } }, - "@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==" + "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" + } }, - "@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/@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" + } }, - "@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/@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" + } }, - "acorn": { + "node_modules/acorn": { "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", - "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==" + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "acorn-walk": { + "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", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==" + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "engines": { + "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" + } }, - "arg": { + "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==" }, - "ayu": { + "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", "integrity": "sha512-yuPZ2kZYQoYaPRQ/78F9rXDVx1rVGCJ1neBYithBoSprD6zPdIJdAKizUXG0jtTBu7nTFyAnVFFYuLnCS3cpDw==", - "requires": { + "dependencies": { "@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==" + "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==" }, - "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": ">=0.6" + } }, - "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==" + "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==" }, - "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/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" + } }, - "deepmerge": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.0.tgz", - "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==" + "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" + } }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + "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" + } }, - "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/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/sindresorhus" + } }, - "nonenumerable": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/nonenumerable/-/nonenumerable-1.1.1.tgz", - "integrity": "sha512-ptUD9w9D8WqW6fuJJkZNCImkf+0vdbgUTbRK3i7jsy3olqtH96hYE6Q/S3Tx9NWbcB/ocAjYshXCAUP0lZ9B4Q==" + "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" + } }, - "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/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" + } }, - "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" + "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" } }, - "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true + "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" + } }, - "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==" + "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" + } }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==" + "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": { + "version": "2.4.2", + "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", + "integrity": "sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og==", + "engines": { + "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", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "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", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dependencies": { + "@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" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "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": "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": ">=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": { + "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==" + }, + "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", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + }, + "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" + } + }, + "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 + } + } } } } diff --git a/styles/package.json b/styles/package.json index 2a0881863b588e2f26424e380173d2fa2de16533..16e95d90d5bebb18e7cfffe88e8d0098b48eb00f 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 13a6951a8288bfd2349dc466364bb4df517f08cf..0000000000000000000000000000000000000000 --- 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 8d807d62f3f597457cbff1e0544390d4ef37bb71..0000000000000000000000000000000000000000 --- 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 0cf1ea037e779818d88d05383b1b8fffd6337ade..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..76c18dfee1fcb63e346d7bad8f093d57fae97dbe --- /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 0000000000000000000000000000000000000000..17575663a1f88b17870b1b146b47e7086bf3e2ba --- /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 0000000000000000000000000000000000000000..fd6aa18ced50af53b6bcf4c3c386d1774c7ab00d --- /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 0000000000000000000000000000000000000000..5d7aa6e0ad259e62b60494f55436c70388017e14 --- /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 ee47bcc6bdc01a0f64557374bcc66febd9a97873..054b2837914c89d82ffc584a1f5ba14e5a34f8ae 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 0000000000000000000000000000000000000000..6887fc7c30e1f234fd043bb115c2658039b5f806 --- /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 0000000000000000000000000000000000000000..58b2a1cbf2ff31a8e4fc50b9e1165bca78ec6b4a --- /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 0000000000000000000000000000000000000000..81c911c7bd6852a0919afadcd2ca27114de152f6 --- /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 0000000000000000000000000000000000000000..0e0013fc071ae36e8c80513b56b9cb7d12764ee8 --- /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 0000000000000000000000000000000000000000..59ccff40f7c71d0997eb4c112c89d1e316096f32 --- /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 0000000000000000000000000000000000000000..8018ce10393fb6145f7214e0deb9620f2b627181 --- /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 0000000000000000000000000000000000000000..c3cde46d65962e90e966435c9bc07d5d4d023d93 --- /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 6244cbae102b2a0d44d0494438182b07c1144f4a..0000000000000000000000000000000000000000 --- 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 bbb4aae5e1b9d35f21f2d78897f368691c006e72..0000000000000000000000000000000000000000 --- 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 c49e1f194c7fbcd16a4f1ac0a5bc7f996fd53dcc..0000000000000000000000000000000000000000 --- 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 e45647c3d62576f9a2db9644bf635f0562b74cef..0000000000000000000000000000000000000000 --- 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 a597e44d9f38ba54cc5ae706c0d61ab7822cb5c8..0000000000000000000000000000000000000000 --- 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 85a0b9d0de1bb61f5d2f67a82da93a509ac4b9be..0000000000000000000000000000000000000000 --- 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 5946bfb82cf01712d19453a8b3f57018f8eac2b8..0000000000000000000000000000000000000000 --- 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 f14cd90219b5f5eddf5dd91fc64e01e120581f1f..0000000000000000000000000000000000000000 --- 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 8614cb69764e06a3c64ac304ec028366a5315eeb..0000000000000000000000000000000000000000 --- 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 859f9fe1b9345df04c8d40821e76743e899c0ef1..0000000000000000000000000000000000000000 --- 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 5eef4b42792131b9b4de128c708d5e17dd28dc14..0000000000000000000000000000000000000000 --- 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 f8988f1f3a42a2791ad70f2128f4454283dc744c..0000000000000000000000000000000000000000 --- 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 c42558059c7b74be4e384c169049cf0c0155a956..0000000000000000000000000000000000000000 --- 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 d84bd6fc7a2cec4a818f920bfc62040c638e4b71..0000000000000000000000000000000000000000 --- 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 cf0f07dd8c519d6c8476dabc71fb57bc1e21971f..0000000000000000000000000000000000000000 --- 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 a86ae010b605fc26f206cdc4219d9594c496e079..0000000000000000000000000000000000000000 --- 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 d05eb1b0c51953b34ac7b08727c81447a66bc047..0000000000000000000000000000000000000000 --- 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 d69c4bb2d919d2b70054deba22f16e3db9eb39f6..0000000000000000000000000000000000000000 --- 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 2563e718ff2a6d3013f4bc235901aa06cb32b4ba..0000000000000000000000000000000000000000 --- 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 8d88f05c53466b718d5efd189cfdf6a321499ba2..0000000000000000000000000000000000000000 --- 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 a8d926f40e44d0f372278fae7961e70693bf605e..0000000000000000000000000000000000000000 --- 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 c5b397b34a3e176207b75f965aadbceeb4611e37..0000000000000000000000000000000000000000 --- 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 8a7eb7a549a18acbb17b115c8306e245e7a8dec3..0000000000000000000000000000000000000000 --- 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 92616eb0223781deae3162bfd7d429a3f896e6eb..0000000000000000000000000000000000000000 --- 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 1666ce5658776bd8c3072ee98a3233bcb8cf5a68..0000000000000000000000000000000000000000 --- 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 281012e62f562ac721ab17c4c72afb458e1d59f1..0000000000000000000000000000000000000000 --- 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 10e6e02b953addb6e44165b1a387eb284e852d1c..0000000000000000000000000000000000000000 --- 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 ae8de178f88c5abfb3fb76af4cda4e386ff748d1..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..ccfdd60a981f4d5e763ce35902b0456bf0d703cd --- /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 0000000000000000000000000000000000000000..6df02a0e330bc24ebc01e7b216be1ed64e20ecd3 --- /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 0000000000000000000000000000000000000000..2f7404c8d4ed2b909e29a4086c7a2339645fea7d --- /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 a575dad52725d77dff8951be97a4fb7eb0949449..43a5fa9d28e2c96f25a9975db15d139bee4ff897 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 0000000000000000000000000000000000000000..aa88a9f26a9462106a999325f887fe747b5fe6ed --- /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 0000000000000000000000000000000000000000..1955231f59514847e59248c42f8d301895e38462 --- /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 0000000000000000000000000000000000000000..365e3a646ddf71c8f0ba522fe092f9e951d04cd4 --- /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 0000000000000000000000000000000000000000..0ce63d088a72028e6e221cc5739adfe2376d608d --- /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 0000000000000000000000000000000000000000..d4266a71fe6f3974dcf8a154002f658fadf38d07 --- /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 0000000000000000000000000000000000000000..f002db5ef586a39abfe3e8698cf4e5ddafb390b3 --- /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 0000000000000000000000000000000000000000..a1ba0be43d56b4a81060067b2cc743fe8c6c0562 --- /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 0000000000000000000000000000000000000000..2bb63e951be4764a6cdb33544abf404a84a11d7e --- /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 0000000000000000000000000000000000000000..80f22503496771bf39d8a271c3cfe66988a4b96f --- /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 0000000000000000000000000000000000000000..294ec00a73152f4a79f96ba2e0866cc25baf5fa7 --- /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 0000000000000000000000000000000000000000..bbd664397fa0868e538dd6b4268563e97160fede --- /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 0000000000000000000000000000000000000000..1c13b31a4af969518d7d06f4668334480a84bdd4 --- /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 0000000000000000000000000000000000000000..af997d0a6e8099a0b8449bf0685df1dd4a0a4eda --- /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 0000000000000000000000000000000000000000..e7c1dcedd513ecaf07d16464c8510ccd93d36df8 --- /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 0000000000000000000000000000000000000000..5c16d03233ee7af3f22fb3c67bf9b5b4a69f524d --- /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 0000000000000000000000000000000000000000..aca7fd7f0778d6984cf5d26aea7cab007fad8ebb --- /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 0000000000000000000000000000000000000000..35133f04a2de39cb722b87784f77e08f270fcb78 --- /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 0000000000000000000000000000000000000000..9aeea866f3b33c738e6630c54ce92b21408cb6f2 --- /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 0000000000000000000000000000000000000000..29769f9bae27ba5cc69cf81dae95f915051f4bd8 --- /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 0000000000000000000000000000000000000000..5b98eebfcdb416dfd0acdfd7f15076785eec59b7 --- /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 0000000000000000000000000000000000000000..60894b08f631a432933b9253cc7b2b8e740666a1 --- /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 0000000000000000000000000000000000000000..97f29ab18c0b873f68b4b020bb2722cadfaa1fbc --- /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 0000000000000000000000000000000000000000..54a2d7b78de071f316ee0151a2f99e7f4198d842 --- /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 0000000000000000000000000000000000000000..2d0c36d74cce4514fb48c915f249276c44bc628a --- /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 0000000000000000000000000000000000000000..8ff15d5d26fd05856747eaa898df9cc052b6f2b7 --- /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 0000000000000000000000000000000000000000..5aee3c987d34b75d25db5bc9ad69f4d2a0938f7b --- /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 998f95a636383d9e2c622133f2e2c09a17336672..0000000000000000000000000000000000000000 --- 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 b24f2948cf3f2b6e4c47ee36d4dd5ed9934fa28b..0000000000000000000000000000000000000000 --- 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 40f7a9154c18d7b7a6cabba474eb2befb56af492..0000000000000000000000000000000000000000 --- 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 6c0b53c35b08284643d316bbb0f49161c8ab25f8..0000000000000000000000000000000000000000 --- 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 02002dbe9befd605b3a57400684b2f37bf7b642b..0000000000000000000000000000000000000000 --- 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 619b0795c89770920b886c20a508da23d9b6eed9..0000000000000000000000000000000000000000 --- 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 8de65a37eb131ba412fe269529106d58070669ee..0000000000000000000000000000000000000000 --- 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 58ee4ccc7c236f4312b3dd5658c0cb5acff7030c..83c2107483664986819a1f0e7c2eb42ac2c4b2d7 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 9a810730867ad82fa57265a8232ab397a51d84f4..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..dff4c3dbc47c9a3542304d8544bb577e3fed22f6 --- /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 2bf625521c19711178278d28feb88e0bba86f773..ca8aaa461fa12204b35596f34056c02c52d740e4 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 f8c44ba3f97e5c24814d5df68ce58a8a97aafb72..c5b915a8c50c02529d136f0d42c9d6c61d832eaa 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 380fd3178617fd167ba911fc49be72f3513ccf27..540a1d0ff9822d5a8c2cf292ddba5ace6839ac6d 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 176ae83bb769f4bdc38f83a64c5963084c56de11..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..bc8f07425f9e676a27d36ec929ea9a116006c694 --- /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 84b8db5ce00a0c1767c0b7326444176f594f56fa..0000000000000000000000000000000000000000 --- 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 3ee813b8c4917d733c3bd91de0728de084f81524..6b4e1d79f761bb389838044d668769f7a12e4f74 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 b5f5538b2eae54c68ca2a49ac7a1e2523ed1251b..4bf605aa93f00e3de3bb84c01c1a9e82f3989d20 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 0000000000000000000000000000000000000000..f759bc813910416d88a2097134dc95654d6ab3b3 --- /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 3e5187dd64c3c9e8c5b57b39c9a4ac9a93f46250..60e846ce94aafbbd98bf17948acfda983420f769 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 bdd549491fac6822878157337aa5dc4d09ef53f2..9422adafa4dbd08333fb027481c2323c1d256872 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 52c29bb2ec2071670c2c0b7f4f6dfd990d834381..18699d21cd9b8118ec18358e8a6cc515c268d002 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 9f92967a0436d7118c20cf29bfb8844dba2699b1..47c46d04295dacb5373d352004286dcf1df8d3f9 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 ebec67b4c276639f22427182bd21dc72e8acbcfb..faf957b6423c04f5997eef566c60e3a72e853af9 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 c1b7a05d4718171a81e4f8c45e0a70ee62e73625..856cd300436de7baa50e81de6e57f03a93a00e11 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 c2ebc424e77c6b30ad693c86e0c932f204f91a20..fb67fd2471a113dfed33847836276cb7cbac7044 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 01cb1d67cba048c2437c44c495a769eeab71d68e..5e9e5b69276562fb8ee5d945282c1ebe44b3e5fc 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 8e32c1f68f5d78d115c7bf4aec999c442d882a67..0badf4371e5a629030765d76f60f1ee7d7078e0f 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 75fcb8e8302d26ab747bb42b3c07cda630fa1c7f..adc77e760715b63c5955b36ab31db141c44e6e89 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 7ee7ae4ab13681a5413dc4cc97f3166c5dbed1a2..3e89518c0bb04095fabd86296853c31a66c0615e 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 17d3b63d8832cf7bd1cee3ba4ae65cae102338be..68d2c50876df452145dc0e5cd6a674370ecf37f5 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 11751367a3f9d01d490d8ec9961e3f0cc092464d..c185d69e43737c420dd1a6809e2cd187bd3c7c60 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 07f4a9b3cb3194abda570408f1642355815a66f6..4414987e229a1d1dccff657c3f5d4de227cb51ba 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 b1c98ddfdf20aa45b614392436e4b764482b3288..7fdc3b4eba363d74614db2f49b0bf512388acacc 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 d960444defa0ef3615c480d7e2d82e5573435089..bdda48f6c732f1b830c32be42002198d205a3699 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 74693b24fd35e7f705ecbd71feb1360aa38e3133..ff287bc80dd6c033f65b086a571321915b2fb0cb 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 dd3130cea0d191966e0c6d4cb4d11fdfa41047b1..8a9fb989ad2105b8f6f594b713b6115904eae141 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 c387ac5ae989ee75fcb5bd8eddfc7fd41840a53f..d94af30334ca378116ae79469e0181b9201c8744 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 64edd406a8982abe6f6b76e043931a0ff4b20c3c..2426b05400a77472264e9e76333d5e4585313da6 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 dbccb96013c8a06cadc05224d9857dbd3637bdd1..abb267f5a4078774ef4efbbd9e0ecf122bc074c3 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 a9c034ed4406c449baeac12a10644704b2d3dc4d..455e7795e145dcca3c84a719671253a55aef26be 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 edfc518b8e9d4972bdc5132f6e2e4811ad2166b4..3f33647daa9414b6619005b7d0e6931e718d17c8 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 fbef6683bf99d83a9ba1c125c4a0790d7ce23981..2cb4d0453952135265adf349da176fe772c98811 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 2e5be61f52d92d32a58edb6d82517cabd6a0957d..b76ccc5b607a2f149a35114a519ddaaf2d1b254d 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 6b83ef0582f26b04f37f8b78ef5a3121b3f3a326..37a92292688fe679502eaa6be87ae5e2ce09eb03 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 7feddacd2b1543ecebba174709d3c8d59f12a278..a12ce08e29e1d905ade51c12803f324c0c5f7678 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 bf023852471bdcbfe109127ce679c60695fdccaa..aceda0d01751ec69253278c983531f2f3435c614 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 d2a69e7ab6a52fb8664516e4d3ee85b36fe19551..9dd3ea7a6133e875faa7727cc54b11d45e94b6fd 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 1eb2c91916d3e374508cf4d0365046583c844e01..2bd0bbf259aef2d9fc6c084f1da3c72379927026 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 2a9230614399a48916e74cfb74bd4625686c7bcb..0e18d6d7a93b8054677b18cbe8d0d1e914718452 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 18e8c5b97e5e75f050f83ff9605f3a68ca0bf110..2fa6b58faadb91b5689c5eac93baf706f6faa391 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 4102671189e856bbd0c508fc1d6be88ee55e8c0c..72757c99f2425cb0fab24c5be23d5b888bcd5816 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 d550d63768d40aa97295c9a85922b3788cbf2f96..d8f63ed3311ea2eb6e7b50d9d6c9e2e5f84d788c 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 05850028a40cb583d8a3beb7b706c52e0523e81c..0582baa0d8b8779c5e595efdf7d127a93c8113e8 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 ec3cddec758379f6bf84b165d04eed31373e34a4..bcaea06a4116ceaae9f88436aa1ef811c2c12895 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 0888e847aca9ffdfb8a7147d058cd44f35e4562c..5eb79f647be57db20886bb22aff821d29a9dc4da 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 6f53ce529965a65a7cb0901ea2703f10b4502922..dc54a33f2644469878773193bb0cb67ba37c5cc5 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 75853bc042747ac9138a6e3fb47c3ac5e98a2e6c..72bb100e7b823e6d37aea5f7d8d8c84fa8e5b362 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 dc07dc10ad0de56ebe0bfad8d65e82f0f5d627ef..f7637d33eafbc01c0ec8770eb9d900ddd8bca64b 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 69a5bd55755a55cce1fd6128a5a8922d3ce7cf31..f672b892ee040e60745f3d0f8bd6875c743ed46a 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 9123c8879d1bcc11f9258fc538aa4b377c2a3ec5..c3de7826c96679fb1c1f2b1a9d81324687d919b9 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 dfd60136f95374fbe3e112a6051a4854b61ac4ec..12767334539540f92889031d8682d2487acc28bd 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 0000000000000000000000000000000000000000..5c5482a754a634bd2338af2613327554ec36d13c --- /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 a373ed378c0ccc8b07da7303a504aff3906a59b6..c78f1132dd34c2acc339a2f974a3d2087d2da7d6 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 94b8166cb30e3eb6cfe19a45532fbe0c7cb2e7ab..450d6865e7e8ddb193cb97efaae504ea157ade97 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 3aabe3f10e78b44603151c8225deacbb9ae7f227..b305b5b5775fa3a58fb3fabdb8f7add00ab3de83 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 c66a06c51b46671cfe20194ac8ff545683c7a7e3..ba6559d8106fb5f9dfefc2cd0da8540c86ffdd68 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 753828c66579e48d958066defb3a95aa1431d71c..b54c402e475aba8190b48425e1fac370dde2ce45 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 221eee6f152873e2e6c52ca4a89ac1d65118843b..2b5ddc4158b51df484a9df6e1724b9ef387860b6 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 4084757525cd131933bd6ae874f5cfc1415990fb..05e6f018ab7ac66da4e3eb114d26d82238001488 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 d7525414ad01c246c21e908666064d6db4233901..dd49a64536aea1cecc98e709c923e1d7d69e23f6 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 08098d2e2fd009e8858d8967d6714800dd8d656a..f9037feae49aaee1007f2694ea8620139d414a59 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 62b226cd106876f106e159a1b5a3ffc79fda8d5c..04fd4d53bba14be9b7c5e7d2c89231a0066aaf49 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 519106470783da85ebee364752a7a4e86eb879e7..0000000000000000000000000000000000000000 --- 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 6d24728a0a9f0e38b3a038c7c7bf3d0642e6e88f..281bd74b215bd16426bb6a8f9d68ddeb5a5bea43 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 0000000000000000000000000000000000000000..00f3a9852db6b9a98a836e1c0abb78a4a08f354a --- /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"], + }, +})