diff --git a/Cargo.lock b/Cargo.lock index b5196d351d..c0818ec137 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,13 +44,22 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + [[package]] name = "aligned-vec" version = "0.6.4" @@ -60,15 +69,6 @@ dependencies = [ "equator", ] -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anstream" version = "0.6.21" @@ -101,22 +101,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -125,6 +125,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "ar_archive_writer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c269894b6fe5e9d7ada0cf69b5bf847ff35bc25fc271f08e1d080fce80339a" +dependencies = [ + "object 0.32.2", +] + [[package]] name = "arbitrary" version = "1.4.2" @@ -139,7 +148,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -160,6 +169,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -256,9 +274,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.32" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89bce6054c720275ac2432fbba080a66a2106a44a1b804553930ca6909f4e0" +checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" dependencies = [ "compression-codecs", "compression-core", @@ -320,16 +338,16 @@ dependencies = [ "futures-lite 2.6.1", "parking", "polling", - "rustix 1.1.2", + "rustix 1.1.3", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener 5.4.1", "event-listener-strategy", @@ -362,7 +380,7 @@ dependencies = [ "cfg-if", "event-listener 5.4.1", "futures-lite 2.6.1", - "rustix 1.1.2", + "rustix 1.1.3", ] [[package]] @@ -373,7 +391,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -388,7 +406,7 @@ dependencies = [ "cfg-if", "futures-core", "futures-io", - "rustix 1.1.2", + "rustix 1.1.3", "signal-hook-registry", "slab", "windows-sys 0.61.2", @@ -421,20 +439,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "async-tar" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1937db2d56578aa3919b9bdb0e5100693fd7d1c0f145c53eb81fbb03e217550" -dependencies = [ - "async-std", - "filetime", - "libc", - "pin-project", - "redox_syscall 0.2.16", - "xattr", -] - [[package]] name = "async-task" version = "4.7.1" @@ -449,20 +453,20 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "async_zip" -version = "0.0.18" +version = "0.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" +checksum = "00b9f7252833d5ed4b00aa9604b563529dd5e11de9c23615de2dcdf91eb87b52" dependencies = [ "async-compression", "crc32fast", "futures-lite 2.6.1", "pin-project", - "thiserror 2.0.17", + "thiserror 1.0.69", ] [[package]] @@ -484,15 +488,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "av1-grain" -version = "0.2.4" +name = "av-scenechange" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3efb2ca85bc610acfa917b5aaa36f3fcbebed5b3182d7f877b02531c4b80c8" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.17", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" dependencies = [ "anyhow", "arrayvec", "log", - "nom", + "nom 8.0.0", "num-rational", "v_frame", ] @@ -506,29 +530,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "aws-lc-rs" -version = "1.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "107a4e9d9cab9963e04e84bb8dee0e25f2a987f9a8bad5ed054abd439caa8f8c" -dependencies = [ - "bindgen 0.72.1", - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -539,7 +540,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.37.3", "rustc-demangle", "windows-link 0.2.1", ] @@ -556,10 +557,10 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -567,27 +568,7 @@ dependencies = [ "regex", "rustc-hash 2.1.1", "shlex", - "syn 2.0.106", -] - -[[package]] -name = "bindgen" -version = "0.72.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" -dependencies = [ - "bitflags 2.9.4", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 2.1.1", - "shlex", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -619,15 +600,18 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bitstream-io" -version = "2.6.0" +version = "4.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] [[package]] name = "blade-graphics" @@ -637,7 +621,7 @@ checksum = "e4deb8f595ce7f00dee3543ebf6fd9a20ea86fc421ab79600dac30876250bdae" dependencies = [ "ash", "ash-window", - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "codespan-reporting", "glow", @@ -672,7 +656,7 @@ checksum = "27142319e2f4c264581067eaccb9f80acccdde60d8b4bf57cc50cd3152f109ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -735,9 +719,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "serde", @@ -745,15 +729,15 @@ dependencies = [ [[package]] name = "built" -version = "0.7.7" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytemuck" @@ -772,7 +756,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -789,18 +773,18 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "calloop" version = "0.14.3" source = "git+https://github.com/zed-industries/calloop#eb6b4fd17b9af5ecc226546bdd04185391b3e265" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "polling", - "rustix 1.1.2", + "rustix 1.1.3", "slab", "tracing", ] @@ -812,7 +796,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ "calloop", - "rustix 1.1.2", + "rustix 1.1.3", "wayland-backend", "wayland-client", ] @@ -839,16 +823,16 @@ dependencies = [ "quote", "serde", "serde_json", - "syn 2.0.106", + "syn 2.0.111", "tempfile", "toml 0.8.23", ] [[package]] name = "cc" -version = "1.2.41" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9fe6cdbb24b6ade63616c0a0688e45bb56732262c158df3c0c4bea4ca47cb7" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "jobserver", @@ -856,29 +840,13 @@ dependencies = [ "shlex", ] -[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ - "nom", -] - -[[package]] -name = "cfg-expr" -version = "0.15.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" -dependencies = [ - "smallvec", - "target-lexicon", + "nom 7.1.3", ] [[package]] @@ -902,20 +870,6 @@ dependencies = [ "libc", ] -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - [[package]] name = "cipher" version = "0.4.4" @@ -944,15 +898,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "cocoa" version = "0.25.0" @@ -975,7 +920,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "cocoa-foundation 0.2.0", "core-foundation 0.10.0", @@ -1005,7 +950,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -1024,14 +969,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "collections" -version = "0.1.0" -dependencies = [ - "indexmap", - "rustc-hash 2.1.1", -] - [[package]] name = "color_quant" version = "1.1.0" @@ -1044,16 +981,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "memchr", -] - [[package]] name = "command-fds" version = "0.3.2" @@ -1066,9 +993,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.31" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef8a506ec4b81c460798f572caead636d57d3d7e940f998160f52bd254bf2d23" +checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" dependencies = [ "compression-core", "deflate64", @@ -1078,9 +1005,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.29" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e47641d3deaf41fb1538ac1f54735925e275eaf3bf4d55c81b137fba797e5cbb" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "concurrent-queue" @@ -1162,7 +1089,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", @@ -1175,7 +1102,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32eb7c354ae9f6d437a6039099ce7ecd049337a8109b23d73e48e8ffba8e9cd5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "core-graphics-types 0.1.3", "foreign-types", @@ -1199,7 +1126,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.0", "libc", ] @@ -1210,7 +1137,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "cfg-if", "core-foundation 0.10.0", @@ -1243,6 +1170,15 @@ dependencies = [ "metal", ] +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "core_maths" version = "0.1.1" @@ -1258,7 +1194,7 @@ version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da46a9d5a8905cc538a4a5bceb6a4510de7a51049c5588c0114efce102bcbbe8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "fontdb 0.16.2", "log", "rangemap", @@ -1335,9 +1271,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "rand_core 0.6.4", @@ -1382,16 +1318,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", -] - -[[package]] -name = "derive_refineable" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1464,7 +1391,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", ] @@ -1476,7 +1403,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1548,7 +1475,7 @@ dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.8", + "toml 0.9.10+spec-1.1.0", "vswhom", "winreg", ] @@ -1564,9 +1491,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -1586,7 +1513,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1629,7 +1556,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1640,9 +1567,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" dependencies = [ "serde", "serde_core", @@ -1707,9 +1634,9 @@ dependencies = [ [[package]] name = "exr" -version = "1.73.0" +version = "1.74.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" dependencies = [ "bit_field", "half", @@ -1752,7 +1679,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1789,15 +1716,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "flate2" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "miniz_oxide", @@ -1847,9 +1774,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "font-types" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "511e2c18a516c666d27867d2f9821f76e7d591f762e9fc41dd6cc5c90fe54b0b" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" dependencies = [ "bytemuck", ] @@ -1909,7 +1836,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -1938,12 +1865,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futf" version = "0.1.5" @@ -2038,7 +1959,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2087,7 +2008,7 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "windows-link 0.2.1", ] @@ -2120,9 +2041,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.13.3" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" dependencies = [ "color_quant", "weezl", @@ -2136,11 +2057,11 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "git2" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb07a133b1520dc1a5690e9bd08950108873d7ed5de38dcc74d3b5ebffa110" +checksum = "3e2b37e2f62729cdada11f0e6b3b6fe383c69c29fc619e391223e12856af308c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", "libgit2-sys", "log", @@ -2155,9 +2076,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "globset" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" dependencies = [ "aho-corasick", "bstr", @@ -2196,7 +2117,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "gpu-alloc-types", ] @@ -2217,20 +2138,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] -name = "gpui" -version = "0.2.2" +name = "gpui-ce" +version = "0.3.3" dependencies = [ "anyhow", "as-raw-xcb-connection", "ashpd 0.11.0", "async-task", "backtrace", - "bindgen 0.71.1", - "bitflags 2.9.4", + "bindgen", + "bitflags 2.10.0", "blade-graphics", "blade-macros", "blade-util", @@ -2242,7 +2163,6 @@ dependencies = [ "circular-buffer", "cocoa 0.26.0", "cocoa-foundation 0.2.0", - "collections", "core-foundation 0.10.0", "core-foundation-sys", "core-graphics 0.24.0", @@ -2258,8 +2178,14 @@ dependencies = [ "flume", "foreign-types", "futures", - "gpui_macros", - "http_client", + "gpui-macros", + "gpui_collections", + "gpui_http_client", + "gpui_media", + "gpui_refineable", + "gpui_sum_tree", + "gpui_util", + "gpui_util_macros", "image", "inventory", "itertools 0.14.0", @@ -2267,7 +2193,6 @@ dependencies = [ "log", "lyon", "mach2", - "media", "metal", "naga", "num_cpus", @@ -2285,8 +2210,6 @@ dependencies = [ "profiling", "rand 0.9.2", "raw-window-handle", - "refineable", - "reqwest_client", "resvg", "schemars", "seahash", @@ -2299,13 +2222,10 @@ dependencies = [ "spin 0.10.0", "stacksafe", "strum 0.27.2", - "sum_tree", "taffy", "thiserror 2.0.17", "unicode-segmentation", "usvg", - "util", - "util_macros", "uuid", "waker-fn", "wayland-backend", @@ -2327,24 +2247,159 @@ dependencies = [ ] [[package]] -name = "gpui_macros" -version = "0.1.0" +name = "gpui-macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb02dd63a2859714ac7b6b476937617c3c744157af1b49f7c904023a79039be" dependencies = [ - "gpui", "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] -name = "gpui_tokio" -version = "0.1.0" +name = "gpui_collections" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae39dc6d3d201be97e4bc08d96dbef2bc5b5c3d5734e05786e8cc3043342351c" +dependencies = [ + "indexmap", + "rustc-hash 2.1.1", +] + +[[package]] +name = "gpui_derive_refineable" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644de174341a87b3478bd65b66bca38af868bcf2b2e865700523734f83cfc664" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "gpui_http_client" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23822b0a6d2c5e6a42507980a0ab3848610ea908942c8ef98187f646f690335e" dependencies = [ "anyhow", - "gpui", - "tokio", - "util", + "async-compression", + "async-fs", + "bytes", + "derive_more", + "futures", + "gpui_util", + "http", + "http-body", + "log", + "parking_lot", + "serde", + "serde_json", + "sha2", + "tempfile", + "url", + "zed-async-tar", + "zed-reqwest", +] + +[[package]] +name = "gpui_media" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cb8912ae17371725132d2b7eec6797a255accc95d58ee5c1134b529810f14b" +dependencies = [ + "anyhow", + "bindgen", + "core-foundation 0.10.0", + "core-video", + "ctor", + "foreign-types", + "metal", + "objc", +] + +[[package]] +name = "gpui_perf" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40a0961dcf598955130e867f4b731150a20546427b41b1a63767c1037a86d77" +dependencies = [ + "gpui_collections", + "serde", + "serde_json", +] + +[[package]] +name = "gpui_refineable" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258cb099254e9468181aee5614410fba61db4ae115fc1d51b4a0b985f60d6641" +dependencies = [ + "gpui_derive_refineable", +] + +[[package]] +name = "gpui_sum_tree" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f3bedd573fafafa13d1200b356c588cf094fb2786e3684bb3f5ea59b549fa9" +dependencies = [ + "arrayvec", + "log", + "rayon", +] + +[[package]] +name = "gpui_util" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68faea25903ae524de9af83990b9aa51bcbc8dd085929ac0aea7fd41905e05c3" +dependencies = [ + "anyhow", + "async-fs", + "async_zip", + "command-fds", + "dirs 4.0.0", + "dunce", + "futures", + "futures-lite 1.13.0", + "git2", + "globset", + "gpui_collections", + "gpui_util_macros", + "itertools 0.14.0", + "libc", + "log", + "nix 0.29.0", + "rand 0.9.2", + "regex", + "rust-embed", + "schemars", + "serde", + "serde_json", + "serde_json_lenient", + "shlex", + "smol", + "take-until", + "tempfile", + "tendril", + "unicase", + "walkdir", + "which", +] + +[[package]] +name = "gpui_util_macros" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c28f65ef47fb97e21e82fd4dd75ccc2506eda010c846dc8054015ea234f1a22" +dependencies = [ + "gpui_perf", + "quote", + "syn 2.0.111", ] [[package]] @@ -2466,47 +2521,20 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "html5ever" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" -dependencies = [ - "log", - "mac", - "markup5ever", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "html_to_markdown" -version = "0.1.0" -dependencies = [ - "anyhow", - "html5ever", - "indoc", - "markup5ever_rcdom", - "pretty_assertions", - "regex", + "windows-sys 0.61.2", ] [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2533,38 +2561,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http_client" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-fs", - "async-tar", - "bytes", - "derive_more", - "futures", - "http", - "http-body", - "log", - "parking_lot", - "serde", - "serde_json", - "serde_urlencoded", - "sha2", - "tempfile", - "url", - "util", -] - -[[package]] -name = "http_client_tls" -version = "0.1.0" -dependencies = [ - "rustls", - "rustls-platform-verifier", -] - [[package]] name = "httparse" version = "1.10.1" @@ -2573,9 +2569,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", "bytes", @@ -2612,9 +2608,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "bytes", "futures-channel", @@ -2631,35 +2627,11 @@ dependencies = [ "tracing", ] -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -2670,9 +2642,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -2683,11 +2655,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -2698,42 +2669,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2764,9 +2731,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.8" +version = "0.25.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" dependencies = [ "bytemuck", "byteorder-lite", @@ -2782,8 +2749,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core", - "zune-jpeg", + "zune-core 0.5.0", + "zune-jpeg 0.5.8", ] [[package]] @@ -2810,9 +2777,9 @@ checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" [[package]] name = "indexmap" -version = "2.11.4" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", @@ -2820,12 +2787,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "indoc" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" - [[package]] name = "inout" version = "0.1.4" @@ -2853,7 +2814,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -2904,15 +2865,15 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -2928,56 +2889,34 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "jiff" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" dependencies = [ "jiff-static", "log", "portable-atomic", "portable-atomic-util", - "serde", + "serde_core", ] [[package]] name = "jiff-static" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[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.34" @@ -2990,9 +2929,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -3060,9 +2999,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libfuzzer-sys" @@ -3076,9 +3015,9 @@ dependencies = [ [[package]] name = "libgit2-sys" -version = "0.18.2+1.9.1" +version = "0.18.3+1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c42fe03df2bd3c53a3a9c7317ad91d80c81cd1fb0caec8d7cc4cd2bfa10c222" +checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487" dependencies = [ "cc", "libc", @@ -3104,20 +3043,20 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "libc", - "redox_syscall 0.5.18", + "redox_syscall 0.6.0", ] [[package]] name = "libz-sys" -version = "1.1.22" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" dependencies = [ "cc", "libc", @@ -3139,9 +3078,9 @@ checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" @@ -3154,11 +3093,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ - "serde", + "serde_core", "value-bag", ] @@ -3210,9 +3149,9 @@ dependencies = [ [[package]] name = "lyon_geom" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e16770d760c7848b0c1c2d209101e408207a65168109509f8483837a36cf2e7" +checksum = "e260b6de923e6e47adfedf6243013a7a874684165a6a277594ee3906021b2343" dependencies = [ "arrayvec", "euclid", @@ -3264,32 +3203,6 @@ dependencies = [ "libc", ] -[[package]] -name = "markup5ever" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" -dependencies = [ - "log", - "phf", - "phf_codegen", - "string_cache", - "string_cache_codegen", - "tendril", -] - -[[package]] -name = "markup5ever_rcdom" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" -dependencies = [ - "html5ever", - "markup5ever", - "tendril", - "xml5ever", -] - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -3310,20 +3223,6 @@ dependencies = [ "digest", ] -[[package]] -name = "media" -version = "0.1.0" -dependencies = [ - "anyhow", - "bindgen 0.71.1", - "core-foundation 0.10.0", - "core-video", - "ctor", - "foreign-types", - "metal", - "objc", -] - [[package]] name = "memchr" version = "2.7.6" @@ -3332,9 +3231,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" dependencies = [ "libc", ] @@ -3354,7 +3253,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block", "core-graphics-types 0.1.3", "foreign-types", @@ -3403,9 +3302,9 @@ checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -3414,9 +3313,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.7" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" dependencies = [ "num-traits", "pxfm", @@ -3430,7 +3329,7 @@ checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632" dependencies = [ "arrayvec", "bit-set", - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg_aliases", "codespan-reporting", "half", @@ -3468,7 +3367,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -3480,7 +3379,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "cfg-if", "cfg_aliases", "libc", @@ -3497,6 +3396,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "noop_proc_macro" version = "0.3.0" @@ -3505,9 +3413,9 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" [[package]] name = "ntapi" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +checksum = "c70f219e21142367c70c0b30c6a9e3a14d55b4d12a204d897fbec83a0363f081" dependencies = [ "winapi", ] @@ -3570,7 +3478,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -3656,11 +3564,11 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3673,7 +3581,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "dispatch2", "objc2", ] @@ -3686,22 +3594,22 @@ checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" [[package]] name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", ] [[package]] name = "objc2-metal" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "block2", "dispatch2", "objc2", @@ -3711,11 +3619,11 @@ dependencies = [ [[package]] name = "objc2-quartz-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3724,11 +3632,11 @@ dependencies = [ [[package]] name = "objc2-ui-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "objc2", "objc2-core-foundation", "objc2-foundation", @@ -3753,6 +3661,15 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "object" version = "0.37.3" @@ -3770,9 +3687,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "oo7" @@ -3811,9 +3728,9 @@ dependencies = [ [[package]] name = "open" -version = "5.3.2" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "is-wsl", "libc", @@ -3877,6 +3794,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + [[package]] name = "pathdiff" version = "0.2.3" @@ -3918,53 +3841,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "perf" -version = "0.1.0" -dependencies = [ - "collections", - "serde", - "serde_json", -] - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator", - "phf_shared", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared", - "rand 0.8.5", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -3988,7 +3864,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4039,7 +3915,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "crc32fast", "fdeflate", "flate2", @@ -4056,7 +3932,7 @@ dependencies = [ "concurrent-queue", "hermit-abi", "pin-project-lite", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -4068,9 +3944,9 @@ checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" [[package]] name = "portable-atomic-util" @@ -4100,9 +3976,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -4116,12 +3992,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - [[package]] name = "pretty_assertions" version = "1.4.1" @@ -4139,7 +4009,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4148,7 +4018,7 @@ version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -4170,14 +4040,14 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] @@ -4198,23 +4068,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "psm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e66fcd288453b748497d8fb18bccc83a16b0518e3906d4b8df0a8d42d93dbb1c" +checksum = "d11f2fedc3b7dafdc2851bc52f277377c5473d378859be234bc7ebb593144d01" dependencies = [ + "ar_archive_writer", "cc", ] [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" dependencies = [ "num-traits", ] @@ -4309,9 +4180,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -4383,25 +4254,27 @@ dependencies = [ [[package]] name = "rangemap" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68" [[package]] name = "rav1e" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" dependencies = [ + "aligned-vec", "arbitrary", "arg_enum_proc_macro", "arrayvec", + "av-scenechange", "av1-grain", "bitstream-io", "built", "cfg-if", "interpolate_name", - "itertools 0.12.1", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -4410,23 +4283,21 @@ dependencies = [ "noop_proc_macro", "num-derive", "num-traits", - "once_cell", "paste", "profiling", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand 0.9.2", + "rand_chacha 0.9.0", "simd_helpers", - "system-deps", - "thiserror 1.0.69", + "thiserror 2.0.17", "v_frame", "wasm-bindgen", ] [[package]] name = "ravif" -version = "0.11.20" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" dependencies = [ "avif-serialize", "imgref", @@ -4500,7 +4371,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", +] + +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags 2.10.0", ] [[package]] @@ -4531,14 +4411,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "refineable" -version = "0.1.0" -dependencies = [ - "derive_refineable", + "syn 2.0.111", ] [[package]] @@ -4570,23 +4443,6 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "reqwest_client" -version = "0.1.0" -dependencies = [ - "anyhow", - "bytes", - "futures", - "gpui", - "http_client", - "http_client_tls", - "log", - "regex", - "serde", - "tokio", - "zed-reqwest", -] - [[package]] name = "resvg" version = "0.45.1" @@ -4632,9 +4488,9 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -4643,22 +4499,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.106", + "syn 2.0.111", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "globset", "sha2", @@ -4698,7 +4554,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -4707,11 +4563,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys 0.11.0", @@ -4720,12 +4576,10 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.33" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "751e04a496ca00bb97a5e043158d23d66b5aabf2e1d5aa2a0aaebb1aafe6f82c" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ - "aws-lc-rs", - "log", "once_cell", "ring", "rustls-pki-types", @@ -4757,48 +4611,20 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ "web-time", "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" -dependencies = [ - "core-foundation 0.10.0", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -4816,7 +4642,7 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "libm", "smallvec", @@ -4833,7 +4659,7 @@ version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "bytemuck", "core_maths", "log", @@ -4847,9 +4673,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" [[package]] name = "same-file" @@ -4869,23 +4695,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "scheduler" -version = "0.1.0" -dependencies = [ - "async-task", - "backtrace", - "chrono", - "futures", - "parking_lot", - "rand 0.9.2", -] - [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "54e910108742c57a770f492731f99be216a52fadd361b06c8fb59d74ccc267d2" dependencies = [ "dyn-clone", "indexmap", @@ -4897,14 +4711,14 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "1.0.4" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80" +checksum = "4908ad288c5035a8eb12cfdf0d49270def0a268ee162b75eeee0f85d155a7c45" dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -4954,7 +4768,7 @@ version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.10.0", "core-foundation-sys", "libc", @@ -4973,9 +4787,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749" +checksum = "16c2f82143577edb4921b71ede051dac62ca3c16084e918bf7b40c96ae10eb33" [[package]] name = "semver" @@ -5014,7 +4828,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5025,30 +4839,30 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "serde_fmt" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d4ddca14104cd60529e8c7f7ba71a2c8acd8f7f5cfcdc2faf97eeb7c3010a4" +checksum = "6e497af288b3b95d067a23a4f749f2861121ffcb2f6d8379310dcda040c345ed" dependencies = [ - "serde", + "serde_core", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "6af14725505314343e673e9ecb7cd7e8a36aa9791eb936235a3567cc31447ae4" dependencies = [ "indexmap", "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -5072,7 +4886,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5086,9 +4900,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -5130,18 +4944,19 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "simd_helpers" @@ -5185,9 +5000,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "slotmap" -version = "1.0.7" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbff4acf519f630b3a3ddcfaea6c06b42174d9a44bc70c620e9ed1649d58b82a" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" dependencies = [ "version_check", ] @@ -5255,7 +5070,7 @@ version = "0.3.0+sdk-1.3.268.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -5295,7 +5110,7 @@ checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" dependencies = [ "proc-macro-error2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5313,31 +5128,6 @@ dependencies = [ "float-cmp", ] -[[package]] -name = "string_cache" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" -dependencies = [ - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", - "serde", -] - -[[package]] -name = "string_cache_codegen" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", -] - [[package]] name = "strum" version = "0.26.3" @@ -5366,7 +5156,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5378,7 +5168,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5387,29 +5177,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "sum_tree" -version = "0.1.0" -dependencies = [ - "arrayvec", - "ctor", - "log", - "rand 0.9.2", - "rayon", - "tracing", -] - [[package]] name = "sval" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94c4464e595f0284970fd9c7e9013804d035d4a61ab74b113242c874c05814d" +checksum = "502b8906c4736190684646827fbab1e954357dfe541013bbd7994d033d53a1ca" [[package]] name = "sval_buffer" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0f46e34b20a39e6a2bf02b926983149b3af6609fd1ee8a6e63f6f340f3e2164" +checksum = "c4b854348b15b6c441bdd27ce9053569b016a0723eab2d015b1fd8e6abe4f708" dependencies = [ "sval", "sval_ref", @@ -5417,18 +5195,18 @@ dependencies = [ [[package]] name = "sval_dynamic" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d0970e53c92ab5381d3b2db1828da8af945954d4234225f6dd9c3afbcef3f5" +checksum = "a0bd9e8b74410ddad37c6962587c5f9801a2caadba9e11f3f916ee3f31ae4a1f" dependencies = [ "sval", ] [[package]] name = "sval_fmt" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43e5e6e1613e1e7fc2e1a9fdd709622e54c122ceb067a60d170d75efd491a839" +checksum = "6fe17b8deb33a9441280b4266c2d257e166bafbaea6e66b4b34ca139c91766d9" dependencies = [ "itoa", "ryu", @@ -5437,9 +5215,9 @@ dependencies = [ [[package]] name = "sval_json" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec382f7bfa6e367b23c9611f129b94eb7daaf3d8fae45a8d0a0211eb4d4c8e6" +checksum = "854addb048a5bafb1f496c98e0ab5b9b581c3843f03ca07c034ae110d3b7c623" dependencies = [ "itoa", "ryu", @@ -5448,9 +5226,9 @@ dependencies = [ [[package]] name = "sval_nested" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3049d0f99ce6297f8f7d9953b35a0103b7584d8f638de40e64edb7105fa578ae" +checksum = "96cf068f482108ff44ae8013477cb047a1665d5f1a635ad7cf79582c1845dce9" dependencies = [ "sval", "sval_buffer", @@ -5459,18 +5237,18 @@ dependencies = [ [[package]] name = "sval_ref" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f88913e77506085c0a8bf6912bb6558591a960faf5317df6c1d9b227224ca6e1" +checksum = "ed02126365ffe5ab8faa0abd9be54fbe68d03d607cd623725b0a71541f8aaa6f" dependencies = [ "sval", ] [[package]] name = "sval_serde" -version = "2.15.0" +version = "2.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f579fd7254f4be6cd7b450034f856b78523404655848789c451bacc6aa8b387d" +checksum = "a263383c6aa2076c4ef6011d3bae1b356edf6ea2613e3d8e8ebaa7b57dd707d5" dependencies = [ "serde_core", "sval", @@ -5517,9 +5295,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -5543,7 +5321,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5575,7 +5353,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -5590,19 +5368,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "6.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" -dependencies = [ - "cfg-expr", - "heck 0.5.0", - "pkg-config", - "toml 0.8.23", - "version-compare", -] - [[package]] name = "taffy" version = "0.9.0" @@ -5633,22 +5398,16 @@ dependencies = [ "objc", ] -[[package]] -name = "target-lexicon" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" - [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand 2.3.0", "getrandom 0.3.4", "once_cell", - "rustix 1.1.2", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -5698,7 +5457,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5709,7 +5468,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -5723,7 +5482,7 @@ dependencies = [ "half", "quick-error", "weezl", - "zune-jpeg", + "zune-jpeg 0.4.21", ] [[package]] @@ -5763,9 +5522,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -5802,9 +5561,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -5824,9 +5583,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" dependencies = [ "bytes", "futures-core", @@ -5849,14 +5608,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "indexmap", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow", @@ -5873,9 +5632,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] @@ -5896,21 +5655,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime 0.7.3", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "winnow", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] @@ -5923,9 +5682,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" @@ -5956,9 +5715,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -5974,14 +5733,14 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] @@ -6074,9 +5833,9 @@ checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-linebreak" @@ -6086,15 +5845,15 @@ checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-script" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" [[package]] name = "unicode-segmentation" @@ -6177,64 +5936,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "util" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-fs", - "async_zip", - "collections", - "command-fds", - "dirs 4.0.0", - "dunce", - "futures", - "futures-lite 1.13.0", - "git2", - "globset", - "indoc", - "itertools 0.14.0", - "libc", - "log", - "mach2", - "nix 0.29.0", - "pretty_assertions", - "rand 0.9.2", - "regex", - "rust-embed", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "shlex", - "smol", - "take-until", - "tempfile", - "tendril", - "unicase", - "util_macros", - "walkdir", - "which", -] - -[[package]] -name = "util_macros" -version = "0.1.0" -dependencies = [ - "perf", - "quote", - "syn 2.0.106", -] - [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "sha1_smol", "wasm-bindgen", ] @@ -6252,9 +5962,9 @@ dependencies = [ [[package]] name = "value-bag" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -6262,20 +5972,20 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35540706617d373b118d550d41f5dfe0b78a0c195dc13c6815e92e2638432306" +checksum = "16530907bfe2999a1773ca5900a65101e092c70f642f25cc23ca0c43573262c5" dependencies = [ "erased-serde", - "serde", + "serde_core", "serde_fmt", ] [[package]] name = "value-bag-sval2" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fe7e140a2658cc16f7ee7a86e413e803fc8f9b5127adc8755c19f9fefa63a52" +checksum = "d00ae130edd690eaa877e4f40605d534790d1cf1d651e7685bd6a144521b251f" dependencies = [ "sval", "sval_buffer", @@ -6292,12 +6002,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "version-compare" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" - [[package]] name = "version_check" version = "0.9.5" @@ -6366,9 +6070,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -6377,25 +6081,11 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.54" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -6406,9 +6096,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6416,22 +6106,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.104" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -6449,19 +6139,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "watch" -version = "0.1.0" -dependencies = [ - "ctor", - "env_logger", - "futures", - "gpui", - "log", - "parking_lot", - "rand 0.9.2", -] - [[package]] name = "wayland-backend" version = "0.3.11" @@ -6470,7 +6147,7 @@ checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" dependencies = [ "cc", "downcast-rs", - "rustix 1.1.2", + "rustix 1.1.3", "scoped-tls", "smallvec", "wayland-sys", @@ -6482,8 +6159,8 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.4", - "rustix 1.1.2", + "bitflags 2.10.0", + "rustix 1.1.3", "wayland-backend", "wayland-scanner", ] @@ -6494,7 +6171,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447ccc440a881271b19e9989f75726d60faa09b95b0200a9b7eb5cc47c3eeb29" dependencies = [ - "rustix 1.1.2", + "rustix 1.1.3", "wayland-client", "xcursor", ] @@ -6505,7 +6182,7 @@ version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -6517,7 +6194,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -6529,7 +6206,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols 0.31.2", @@ -6542,7 +6219,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols 0.32.9", @@ -6574,9 +6251,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.81" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -6592,20 +6269,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki-root-certs" -version = "0.26.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "weezl" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" [[package]] name = "which" @@ -6720,19 +6388,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-future" version = "0.2.1" @@ -6752,7 +6407,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6763,7 +6418,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6774,7 +6429,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6785,7 +6440,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -6850,15 +6505,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-strings" version = "0.3.1" @@ -6877,24 +6523,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -6940,21 +6568,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.48.5" @@ -7012,12 +6625,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -7036,12 +6643,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -7060,12 +6661,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -7096,12 +6691,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -7120,12 +6709,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -7144,12 +6727,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -7168,12 +6745,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -7194,9 +6765,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -7234,9 +6805,9 @@ checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "x11" @@ -7267,7 +6838,7 @@ dependencies = [ "as-raw-xcb-connection", "gethostname", "libc", - "rustix 1.1.2", + "rustix 1.1.3", "x11rb-protocol", "xcursor", ] @@ -7318,7 +6889,7 @@ name = "xim-parser" version = "0.2.1" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", ] [[package]] @@ -7339,23 +6910,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" -[[package]] -name = "xml5ever" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" -dependencies = [ - "log", - "mac", - "markup5ever", -] - [[package]] name = "xmlwriter" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + [[package]] name = "yansi" version = "1.0.1" @@ -7381,11 +6947,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -7393,13 +6958,13 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -7446,7 +7011,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "zbus_names", "zvariant", "zvariant_utils", @@ -7464,12 +7029,26 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zed-async-tar" +version = "0.5.0-zed" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf4b5f655e29700e473cb1acd914ab112b37b62f96f7e642d5fc6a0c02eb881" +dependencies = [ + "async-std", + "filetime", + "libc", + "pin-project", + "redox_syscall 0.2.16", + "xattr", +] + [[package]] name = "zed-font-kit" version = "0.14.1-zed" source = "git+https://github.com/zed-industries/font-kit?rev=110523127440aefb11ce0cf280ae7c5071337ec5#110523127440aefb11ce0cf280ae7c5071337ec5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.10.0", "byteorder", "core-foundation 0.10.0", "core-graphics 0.24.0", @@ -7491,7 +7070,8 @@ dependencies = [ [[package]] name = "zed-reqwest" version = "0.12.15-zed" -source = "git+https://github.com/zed-industries/reqwest.git?rev=c15662463bda39148ba154100dd44d3fba5873a4#c15662463bda39148ba154100dd44d3fba5873a4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2d05756ff48539950c3282ad7acf3817ad3f08797c205ad1c34a2ce03b9970" dependencies = [ "base64", "bytes", @@ -7579,22 +7159,22 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] @@ -7614,7 +7194,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "synstructure", ] @@ -7635,14 +7215,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -7651,9 +7231,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -7662,21 +7242,33 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", ] +[[package]] +name = "zmij" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0095ecd462946aa3927d9297b63ef82fb9a5316d7a37d134eeb36e58228615a" + [[package]] name = "zune-core" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" +[[package]] +name = "zune-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773" + [[package]] name = "zune-inflate" version = "0.2.54" @@ -7692,7 +7284,16 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ - "zune-core", + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35aee689668bf9bd6f6f3a6c60bb29ba1244b3b43adfd50edd554a371da37d5" +dependencies = [ + "zune-core 0.5.0", ] [[package]] @@ -7719,7 +7320,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.111", "zvariant_utils", ] @@ -7732,6 +7333,6 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", + "syn 2.0.111", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 9c58e8f496..3599a2d902 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,227 +1,232 @@ -[workspace] -resolver = "2" -members = [ - "crates/collections", - "crates/gpui", - "crates/gpui_macros", - "crates/gpui_tokio", - "crates/html_to_markdown", - "crates/http_client", - "crates/http_client_tls", - "crates/media", - "crates/refineable", - "crates/refineable/derive_refineable", - "crates/reqwest_client", - "crates/scheduler", - "crates/sum_tree", - "crates/util", - "crates/util_macros", - "crates/watch", - "tooling/perf", -] - -[workspace.package] -publish = false +[package] +name = "gpui-ce" +version = "0.3.3" edition = "2024" +authors = ["Nathan Sobo "] +description = "A community fork of Zed's GPU-accelerated UI framework" +repository = "https://github.com/gpui-ce/gpui-ce" +publish = true +license = "Apache-2.0" +homepage = "https://gpui-ce.github.io/" +readme = "README.md" +keywords = ["desktop", "gui", "immediate"] +categories = ["gui"] +autoexamples = false -[workspace.dependencies] +[features] +default = ["font-kit", "wayland", "x11", "windows-manifest"] +test-support = [ + "leak-detection", + "collections/test-support", + "util/test-support", + "http_client/test-support", + "wayland", + "x11", +] +inspector = ["gpui_macros/inspector"] +leak-detection = ["backtrace"] +runtime_shaders = [] +macos-blade = [ + "blade-graphics", + "blade-macros", + "blade-util", + "bytemuck", + "objc2", + "objc2-metal", +] +wayland = [ + "bitflags", + "blade-graphics", + "blade-macros", + "blade-util", + "bytemuck", + "ashpd/wayland", + "cosmic-text", + "font-kit", + "calloop-wayland-source", + "wayland-backend", + "wayland-client", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-plasma", + "wayland-protocols-wlr", + "filedescriptor", + "xkbcommon", + "open", +] +x11 = [ + "blade-graphics", + "blade-macros", + "blade-util", + "bytemuck", + "ashpd", + "cosmic-text", + "font-kit", + "as-raw-xcb-connection", + "x11rb", + "xkbcommon", + "xim", + "x11-clipboard", + "filedescriptor", + "open", + "scap?/x11", +] +screen-capture = [ + "scap", +] +windows-manifest = [] -# -# Workspace member crates -# +[lib] +name = "gpui" +path = "src/gpui.rs" +doctest = false -collections = { path = "crates/collections", version = "0.1.0" } -derive_refineable = { path = "crates/refineable/derive_refineable" } -gpui = { path = "crates/gpui", default-features = false } -gpui_macros = { path = "crates/gpui_macros" } -gpui_tokio = { path = "crates/gpui_tokio" } -html_to_markdown = { path = "crates/html_to_markdown" } -http_client = { path = "crates/http_client" } -http_client_tls = { path = "crates/http_client_tls" } -media = { path = "crates/media" } -refineable = { path = "crates/refineable" } -reqwest_client = { path = "crates/reqwest_client" } -scheduler = { path = "crates/scheduler" } -sum_tree = { path = "crates/sum_tree" } -util = { path = "crates/util" } -util_macros = { path = "crates/util_macros" } -watch = { path = "crates/watch" } -perf = { path = "tooling/perf" } - -# -# External crates -# - -aho-corasick = "1.1" -circular-buffer = "1.0" -any_vec = "0.14" +[dependencies] anyhow = "1.0.86" -arrayvec = { version = "0.7.4", features = ["serde"] } -ashpd = { version = "0.11", default-features = false, features = ["async-std"] } -async-compat = "0.2.1" -async-compression = { version = "0.4", features = ["gzip", "futures-io"] } -async-dispatcher = "0.1" -async-fs = "2.1" -async-lock = "2.1" -async-recursion = "1.0.0" -async-tar = "0.5.1" async-task = "4.7" -async-trait = "0.1" -async-tungstenite = "0.31.0" -async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] } -backtrace = "0.3" -base64 = "0.22" -bincode = "1.2.1" -bitflags = "2.6.0" -blade-graphics = { version = "0.7.0" } -blade-macros = { version = "0.3.0" } -blade-util = { version = "0.3.0" } -brotli = "8.0.2" -bytes = "1.0" -cfg-if = "1.0.3" -chrono = { version = "0.4", features = ["serde"] } -cocoa = "=0.26.0" -cocoa-foundation = "=0.2.0" -convert_case = "0.8.0" -core-foundation = "=0.10.0" -core-foundation-sys = "0.8.6" -core-video = { version = "0.4.3", features = ["metal"] } -crossbeam = "0.8.4" +backtrace = { version = "0.3", optional = true } +bitflags = { version = "2.6.0", optional = true } +blade-graphics = { version = "0.7.0", optional = true } +blade-macros = { version = "0.3.0", optional = true } +blade-util = { version = "0.3.0", optional = true } +bytemuck = { version = "1", optional = true } +circular-buffer = "1.0" +collections = { package = "gpui_collections", version = "0.2.2" } ctor = "0.4.0" -dashmap = "6.0" derive_more = "0.99.17" -dirs = "4.0" -env_logger = "0.11" +etagere = "0.2" futures = "0.3" -futures-lite = "1.13" -git2 = { version = "0.20.1", default-features = false } -globset = "0.4" -heck = "0.5" -hex = "0.4.3" -html5ever = "0.27.0" -indoc = "2" -inventory = "0.3.19" -http = "1.1" -http-body = "1.0" -hyper = "0.14" +gpui_macros = { package = "gpui-macros", version = "0.2.2" } +http_client = { package = "gpui_http_client", version = "0.2.2" } image = "0.25.1" -indexmap = { version = "2.7.0", features = ["serde"] } +inventory = "0.3.19" itertools = "0.14.0" libc = "0.2" -linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } -mach2 = "0.5" -markup5ever_rcdom = "0.3.0" -metal = "0.29" -naga = { version = "25.0", features = ["wgsl-in"] } -nix = "0.29" -objc = "0.2" -objc2-foundation = { version = "=0.3.1", default-features = false, features = [ - "NSArray", - "NSAttributedString", - "NSBundle", - "NSCoder", - "NSData", - "NSDate", - "NSDictionary", - "NSEnumerator", - "NSError", - "NSGeometry", - "NSNotification", - "NSNull", - "NSObjCRuntime", - "NSObject", - "NSProcessInfo", - "NSRange", - "NSRunLoop", - "NSString", - "NSURL", - "NSUndoManager", - "NSValue", - "objc2-core-foundation", - "std" -] } -open = "5.0.0" -ordered-float = "2.1.1" -palette = { version = "0.7.5", default-features = false, features = ["std"] } +lyon = "1.0" +num_cpus = "1.13" +parking = "2.0.0" parking_lot = "0.12.1" -pathdiff = "0.2" +pin-project = "1.1.10" postage = { version = "0.5", features = ["futures-traits"] } -pretty_assertions = { version = "1.3.0", features = ["unstable"] } -proc-macro2 = "1.0.93" profiling = "1" -pulldown-cmark = { version = "0.12.0", default-features = false } -quote = "1.0.9" rand = "0.9" -rayon = "1.8" -regex = "1.5" -# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io -reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [ - "charset", - "http2", - "macos-system-configuration", - "multipart", - "rustls-tls-native-roots", - "socks", - "stream", -], package = "zed-reqwest", version = "0.12.15-zed" } -rust-embed = { version = "8.4", features = ["include-exclude"] } -rustc-hash = "2.1.0" -rustls = { version = "0.23.26" } -rustls-platform-verifier = "0.5.0" -# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io -scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" } +raw-window-handle = "0.6" +refineable = { package = "gpui_refineable", version = "0.2.2" } +resvg = { version = "0.45.0", default-features = false, features = [ + "text", + "system-fonts", + "memmap-fonts", +] } schemars = { version = "1.0", features = ["indexmap2"] } +seahash = "4.1" semver = { version = "1.0", features = ["serde"] } serde = { version = "1.0.221", features = ["derive", "rc"] } serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] } -serde_json_lenient = { version = "0.2", features = [ - "preserve_order", - "raw_value", -] } -serde_urlencoded = "0.7" -sha2 = "0.10" -shlex = "1.3.0" -shellexpand = "2.1.0" slotmap = "1.0.6" -smallvec = { version = "1.6", features = ["union", "const_new"] } +smallvec = { version = "1.6", features = ["union"] } smol = "2.0" +spin = "0.10.0" stacksafe = "0.1" -strsim = "0.11" strum = { version = "0.27.2", features = ["derive"] } -subtle = "2.5.0" -syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] } -sys-locale = "0.3.1" -sysinfo = "0.37.0" -take-until = "0.2.0" -tempfile = "3.20.0" +sum_tree = { package = "gpui_sum_tree", version = "0.2.2" } +taffy = "=0.9.0" thiserror = "2.0.12" -time = { version = "0.3", features = [ - "macros", - "parsing", - "serde", - "serde-well-known", - "formatting", - "local-offset", -] } -tokio = { version = "1" } -tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] } -tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] } -tracing = "0.1.40" -unicase = "2.6" -unicode-segmentation = "1.10" -url = "2.2" +util = { package = "gpui_util", version = "0.2.2" } +util_macros = { package = "gpui_util_macros", version = "0.2.2" } +usvg = { version = "0.45.0", default-features = false } uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } -walkdir = "2.5" -which = "6.0.0" -windows-core = "0.61" -yawc = "0.2.5" -zeroize = "1.8" -ciborium = "0.2" +waker-fn = "1.2.0" -[workspace.dependencies.windows] -version = "0.61" -features = [ +[target.'cfg(target_os = "macos")'.dependencies] +block = "0.1" +cocoa = "=0.26.0" +cocoa-foundation = "=0.2.0" +core-foundation = "=0.10.0" +core-foundation-sys = "0.8.6" +core-graphics = "0.24" +core-text = "21" +core-video = { version = "0.4.3", features = ["metal"] } +flume = "0.11" +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true } +foreign-types = "0.5" +mach2 = "0.5" +media = { package = "gpui_media", version = "0.2.2" } +metal = "0.29" +objc = "0.2" +objc2 = { version = "0.6", optional = true } +objc2-metal = { version = "0.3", optional = true } + +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies] +pathfinder_geometry = "0.5" + +[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies] +scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed", optional = true } + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] +flume = "0.11" +oo7 = { version = "0.5.0", default-features = false, features = [ + "async-std", + "native_crypto", +] } + +ashpd = { version = "0.11", default-features = false, features = ["async-std"], optional = true } +blade-graphics = { version = "0.7.0", optional = true } +blade-macros = { version = "0.3.0", optional = true } +blade-util = { version = "0.3.0", optional = true } +bytemuck = { version = "1", optional = true } +cosmic-text = { version = "0.14.0", optional = true } +font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [ + "source-fontconfig-dlopen", +], optional = true } + +calloop = "0.14.3" +filedescriptor = { version = "0.8.2", optional = true } +open = { version = "5.2.0", optional = true } + +calloop-wayland-source = { version = "0.4.1", optional = true } +wayland-backend = { version = "0.3.3", features = [ + "client_system", + "dlopen", +], optional = true } +wayland-client = { version = "0.31.2", optional = true } +wayland-cursor = { version = "0.31.1", optional = true } +wayland-protocols = { version = "0.31.2", features = [ + "client", + "staging", + "unstable", +], optional = true } +wayland-protocols-plasma = { version = "0.2.0", features = [ + "client", +], optional = true } +wayland-protocols-wlr = { version = "0.3.9", features = [ + "client", +], optional = true } + +as-raw-xcb-connection = { version = "1", optional = true } +x11rb = { version = "0.13.1", features = [ + "allow-unsafe-code", + "xkb", + "randr", + "xinput", + "cursor", + "resource_manager", + "sync", +], optional = true } +xkbcommon = { version = "0.8.0", features = [ + "wayland", + "x11", +], optional = true } +xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8", features = [ + "x11rb-xcb", + "x11rb-client", +], package = "zed-xim", version = "0.4.0-zed", optional = true } +x11-clipboard = { version = "0.9.3", optional = true } + +[target.'cfg(target_os = "windows")'.dependencies] +flume = "0.11" +rand = "0.9" +windows = { version = "0.61", features = [ "Foundation_Numerics", "Storage_Search", "Storage_Streams", @@ -268,39 +273,180 @@ features = [ "Win32_UI_Shell_Common", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging", -] +] } +windows-core = "0.61" +windows-numerics = "0.2" +windows-registry = "0.5" + +[dev-dependencies] +backtrace = "0.3" +collections = { package = "gpui_collections", version = "0.2.2", features = ["test-support"] } +env_logger = "0.11" +http_client = { package = "gpui_http_client", version = "0.2.2", features = ["test-support"] } +lyon = { version = "1.0", features = ["extra"] } +pretty_assertions = { version = "1.3.0", features = ["unstable"] } +rand = "0.9" +unicode-segmentation = "1.10" +util = { package = "gpui_util", version = "0.2.2", features = ["test-support"] } + +[target.'cfg(target_os = "windows")'.build-dependencies] +embed-resource = "3.0" +windows-registry = "0.5" + +[target.'cfg(target_os = "macos")'.build-dependencies] +bindgen = "0.71" +cbindgen = { version = "0.28.0", default-features = false } +naga = { version = "25.0", features = ["wgsl-in"] } + +[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies] +naga = { version = "25.0", features = ["wgsl-in"] } [patch.crates-io] calloop = { git = "https://github.com/zed-industries/calloop" } +# ============================================================================ +# Learn Examples - Educational examples for learning GPUI +# ============================================================================ + +[[example]] +name = "interactive_elements" +path = "examples/learn/interactive_elements.rs" + +[[example]] +name = "creating_components" +path = "examples/learn/creating_components.rs" + +[[example]] +name = "layout" +path = "examples/learn/layout.rs" + +[[example]] +name = "styling" +path = "examples/learn/styling.rs" + +[[example]] +name = "async_tasks" +path = "examples/learn/async_tasks.rs" + +[[example]] +name = "custom_drawing" +path = "examples/learn/custom_drawing.rs" + +[[example]] +name = "animation" +path = "examples/learn/animation.rs" + +[[example]] +name = "text" +path = "examples/learn/text.rs" + +# ============================================================================ +# Bench Examples - Performance benchmarks +# ============================================================================ + +[[example]] +name = "data_table" +path = "examples/bench/data_table.rs" + +[[example]] +name = "paths_bench" +path = "examples/bench/paths_bench.rs" + +[[example]] +name = "pattern" +path = "examples/bench/pattern.rs" + +[[example]] +name = "shadow" +path = "examples/bench/shadow.rs" + +# ============================================================================ +# Legacy Examples - Original examples (to be updated) +# ============================================================================ + +[[example]] +name = "focus_visible" +path = "examples/legacy/focus_visible.rs" + +[[example]] +name = "gif_viewer" +path = "examples/legacy/gif_viewer.rs" + +[[example]] +name = "gradient" +path = "examples/legacy/gradient.rs" + +[[example]] +name = "hello_world" +path = "examples/legacy/hello_world.rs" + +[[example]] +name = "image_loading" +path = "examples/legacy/image_loading.rs" + +[[example]] +name = "input" +path = "examples/legacy/input.rs" + +[[example]] +name = "layer_shell" +path = "examples/legacy/layer_shell.rs" + +[[example]] +name = "on_window_close_quit" +path = "examples/legacy/on_window_close_quit.rs" + +[[example]] +name = "opacity" +path = "examples/legacy/opacity.rs" + +[[example]] +name = "scrollable" +path = "examples/legacy/scrollable.rs" + +[[example]] +name = "svg" +path = "examples/legacy/svg/svg.rs" + +[[example]] +name = "tab_stop" +path = "examples/legacy/tab_stop.rs" + +[[example]] +name = "tree" +path = "examples/legacy/tree.rs" + +[[example]] +name = "uniform_list" +path = "examples/legacy/uniform_list.rs" + +[[example]] +name = "window" +path = "examples/legacy/window.rs" + +[[example]] +name = "window_positioning" +path = "examples/legacy/window_positioning.rs" + +[[example]] +name = "window_shadow" +path = "examples/legacy/window_shadow.rs" + [profile.dev] split-debuginfo = "unpacked" -# https://github.com/rust-lang/cargo/issues/16104 incremental = false codegen-units = 16 -# mirror configuration for crates compiled for the build platform -# (without this cargo will compile ~400 crates twice) [profile.dev.build-override] codegen-units = 16 [profile.dev.package] -# proc-macros start -gpui_macros = { opt-level = 3 } -derive_refineable = { opt-level = 3 } -util_macros = { opt-level = 3 } quote = { opt-level = 3 } syn = { opt-level = 3 } proc-macro2 = { opt-level = 3 } -# proc-macros end - taffy = { opt-level = 3 } resvg = { opt-level = 3 } -collections = { codegen-units = 1 } -refineable = { codegen-units = 1 } -reqwest_client = { codegen-units = 1 } - [profile.release] debug = "limited" lto = "thin" @@ -312,54 +458,19 @@ debug = "full" lto = false codegen-units = 16 -[workspace.lints.rust] +[lints.rust] unexpected_cfgs = { level = "allow" } -[workspace.lints.clippy] +[lints.clippy] dbg_macro = "deny" todo = "deny" - -# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454 -# Remove when the lint gets promoted to `suspicious`. declare_interior_mutable_const = "deny" - redundant_clone = "deny" disallowed_methods = "deny" - -# We currently do not restrict any style rules -# as it slows down shipping code to Zed. -# -# Running ./script/clippy can take several minutes, and so it's -# common to skip that step and let CI do it. Any unexpected failures -# (which also take minutes to discover) thus require switching back -# to an old branch, manual fixing, and re-pushing. -# -# In the future we could improve this by either making sure -# Zed can surface clippy errors in diagnostics (in addition to the -# rust-analyzer errors), or by having CI fix style nits automatically. style = { level = "allow", priority = -1 } - -# Individual rules that have violations in the codebase: type_complexity = "allow" let_underscore_future = "allow" - -# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so -# warning on this rule produces a lot of noise. single_range_in_vec_init = "allow" - -# in Rust it can be very tedious to reduce argument count without -# running afoul of the borrow checker. too_many_arguments = "allow" - -# We often have large enum variants yet we rarely actually bother with splitting them up. large_enum_variant = "allow" - -# Boolean expressions can be hard to read, requiring only the minimal form gets in the way nonminimal_bool = "allow" - -[workspace.metadata.cargo-machete] -ignored = [ - "bindgen", - "cbindgen", - "serde", -] \ No newline at end of file diff --git a/README.md b/README.md index 23ce982213..35527a5bb3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,77 @@ # gpui - Community Edition A community fork of [GPUI](https://gpui.rs), Zed's GPU-accelerated UI framework. + +## Usage + +```toml +[dependencies] +gpui = { package = "gpui-ce", version = "0.3" } + +# for test support... +[dev-dependencies] +gpui = { package = "gpui-ce", version = "0.3", features = ["test-support"] } +``` + +Then use `gpui::{import}` as normal. + +--- + +todo: rewrite below... + +# Welcome to GPUI! + +GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework +for Rust, designed to support a wide variety of applications. + +Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example. + +### Dependencies + +GPUI has various system dependencies that it needs in order to work. + +#### macOS + +On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following: + +- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. + +> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. + +- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) + + ```sh + xcode-select --install + ``` + +- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: + + ```sh + sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer + ``` + +## The Big Picture + +GPUI offers three different [registers]() depending on your needs: + +- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information. + +- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering. + +- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. + +Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework. + +## Other Resources + +In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications: + +- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information. + +- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`. + +- An async executor that is integrated with the platform's event loop. See the `executor` module for more information., + +- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. + +Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/crates/gpui/build.rs b/build.rs similarity index 100% rename from crates/gpui/build.rs rename to build.rs diff --git a/crates/collections/Cargo.toml b/crates/collections/Cargo.toml deleted file mode 100644 index 8675504347..0000000000 --- a/crates/collections/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "collections" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "Standard collection type re-exports used by Zed and GPUI" - -[lints] -workspace = true - -[lib] -path = "src/collections.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -indexmap.workspace = true -rustc-hash.workspace = true diff --git a/crates/collections/LICENSE-APACHE b/crates/collections/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/collections/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs deleted file mode 100644 index ea5ea7332f..0000000000 --- a/crates/collections/src/collections.rs +++ /dev/null @@ -1,9 +0,0 @@ -pub type HashMap = FxHashMap; -pub type HashSet = FxHashSet; -pub type IndexMap = indexmap::IndexMap; -pub type IndexSet = indexmap::IndexSet; - -pub use indexmap::Equivalent; -pub use rustc_hash::FxHasher; -pub use rustc_hash::{FxHashMap, FxHashSet}; -pub use std::collections::*; diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml deleted file mode 100644 index 8fc3797868..0000000000 --- a/crates/gpui/Cargo.toml +++ /dev/null @@ -1,332 +0,0 @@ -[package] -name = "gpui" -version = "0.2.2" -edition.workspace = true -authors = ["Nathan Sobo "] -description = "Zed's GPU-accelerated UI framework" -repository = "https://github.com/zed-industries/zed" -publish = true -license = "Apache-2.0" -homepage = "https://gpui.rs" -readme = "README.md" -keywords = ["desktop", "gui", "immediate"] -categories = ["gui"] - - -[lints] -workspace = true - -[features] -default = ["font-kit", "wayland", "x11", "windows-manifest"] -test-support = [ - "leak-detection", - "collections/test-support", - "util/test-support", - "http_client/test-support", - "wayland", - "x11", -] -inspector = ["gpui_macros/inspector"] -leak-detection = ["backtrace"] -runtime_shaders = [] -macos-blade = [ - "blade-graphics", - "blade-macros", - "blade-util", - "bytemuck", - "objc2", - "objc2-metal", -] -wayland = [ - "bitflags", - "blade-graphics", - "blade-macros", - "blade-util", - "bytemuck", - "ashpd/wayland", - "cosmic-text", - "font-kit", - "calloop-wayland-source", - "wayland-backend", - "wayland-client", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-plasma", - "wayland-protocols-wlr", - "filedescriptor", - "xkbcommon", - "open", -] -x11 = [ - "blade-graphics", - "blade-macros", - "blade-util", - "bytemuck", - "ashpd", - "cosmic-text", - "font-kit", - "as-raw-xcb-connection", - "x11rb", - "xkbcommon", - "xim", - "x11-clipboard", - "filedescriptor", - "open", - "scap?/x11", -] -screen-capture = [ - "scap", -] -windows-manifest = [] - -[lib] -path = "src/gpui.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -async-task = "4.7" -backtrace = { workspace = true, optional = true } -bitflags = { workspace = true, optional = true } -blade-graphics = { workspace = true, optional = true } -blade-macros = { workspace = true, optional = true } -blade-util = { workspace = true, optional = true } -bytemuck = { version = "1", optional = true } -collections.workspace = true -ctor.workspace = true -derive_more.workspace = true -etagere = "0.2" -futures.workspace = true -gpui_macros.workspace = true -http_client.workspace = true -image.workspace = true -inventory.workspace = true -itertools.workspace = true -log.workspace = true -num_cpus = "1.13" -parking = "2.0.0" -parking_lot.workspace = true -postage.workspace = true -profiling.workspace = true -rand.workspace = true -raw-window-handle = "0.6" -refineable.workspace = true -resvg = { version = "0.45.0", default-features = false, features = [ - "text", - "system-fonts", - "memmap-fonts", -] } -usvg = { version = "0.45.0", default-features = false } -util_macros.workspace = true -schemars.workspace = true -seahash = "4.1" -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -slotmap.workspace = true -smallvec.workspace = true -smol.workspace = true -stacksafe.workspace = true -strum.workspace = true -sum_tree.workspace = true -taffy = "=0.9.0" -thiserror.workspace = true -util.workspace = true -uuid.workspace = true -waker-fn = "1.2.0" -lyon = "1.0" -libc.workspace = true -pin-project = "1.1.10" -circular-buffer.workspace = true -spin = "0.10.0" - -[target.'cfg(target_os = "macos")'.dependencies] -block = "0.1" -cocoa.workspace = true -cocoa-foundation.workspace = true -core-foundation.workspace = true -core-foundation-sys.workspace = true -core-graphics = "0.24" -core-video.workspace = true -core-text = "21" -# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io -font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true } -foreign-types = "0.5" -log.workspace = true -media.workspace = true -objc.workspace = true -objc2 = { version = "0.6", optional = true } -objc2-metal = { version = "0.3", optional = true } -mach2.workspace = true -#TODO: replace with "objc2" -metal.workspace = true -flume = "0.11" - -[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies] -pathfinder_geometry = "0.5" - -[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies] -scap = { workspace = true, optional = true } - -[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] -# Always used -flume = "0.11" -oo7 = { version = "0.5.0", default-features = false, features = [ - "async-std", - "native_crypto", -] } - -# Used in both windowing options -ashpd = { workspace = true, optional = true } -blade-graphics = { workspace = true, optional = true } -blade-macros = { workspace = true, optional = true } -blade-util = { workspace = true, optional = true } -bytemuck = { version = "1", optional = true } -cosmic-text = { version = "0.14.0", optional = true } -# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io -font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [ - "source-fontconfig-dlopen", -], optional = true } - -calloop = { version = "0.14.3" } -filedescriptor = { version = "0.8.2", optional = true } -open = { version = "5.2.0", optional = true } - -# Wayland -calloop-wayland-source = { version = "0.4.1", optional = true } -wayland-backend = { version = "0.3.3", features = [ - "client_system", - "dlopen", -], optional = true } -wayland-client = { version = "0.31.2", optional = true } -wayland-cursor = { version = "0.31.1", optional = true } -wayland-protocols = { version = "0.31.2", features = [ - "client", - "staging", - "unstable", -], optional = true } -wayland-protocols-plasma = { version = "0.2.0", features = [ - "client", -], optional = true } -wayland-protocols-wlr = { version = "0.3.9", features = [ - "client", -], optional = true } - -# X11 -as-raw-xcb-connection = { version = "1", optional = true } -x11rb = { version = "0.13.1", features = [ - "allow-unsafe-code", - "xkb", - "randr", - "xinput", - "cursor", - "resource_manager", - "sync", -], optional = true } -xkbcommon = { version = "0.8.0", features = [ - "wayland", - "x11", -], optional = true } -# WARNING: If you change this, you must also publish a new version of zed-xim to crates.io -xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8" , features = [ - "x11rb-xcb", - "x11rb-client", -], package = "zed-xim", version = "0.4.0-zed", optional = true } -x11-clipboard = { version = "0.9.3", optional = true } - -[target.'cfg(target_os = "windows")'.dependencies] -flume = "0.11" -rand.workspace = true -windows.workspace = true -windows-core.workspace = true -windows-numerics = "0.2" -windows-registry = "0.5" - -[dev-dependencies] -backtrace.workspace = true -collections = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -http_client = { workspace = true, features = ["test-support"] } -lyon = { version = "1.0", features = ["extra"] } -pretty_assertions.workspace = true -rand.workspace = true -reqwest_client = { workspace = true, features = ["test-support"] } -unicode-segmentation.workspace = true -util = { workspace = true, features = ["test-support"] } - -[target.'cfg(target_os = "windows")'.build-dependencies] -embed-resource = "3.0" -windows-registry = "0.5" - -[target.'cfg(target_os = "macos")'.build-dependencies] -bindgen = "0.71" -cbindgen = { version = "0.28.0", default-features = false } -naga.workspace = true - -[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies] -naga.workspace = true - - -[[example]] -name = "hello_world" -path = "examples/hello_world.rs" - -[[example]] -name = "image" -path = "examples/image/image.rs" - -[[example]] -name = "input" -path = "examples/input.rs" - -[[example]] -name = "on_window_close_quit" -path = "examples/on_window_close_quit.rs" - -[[example]] -name = "opacity" -path = "examples/opacity.rs" - -[[example]] -name = "pattern" -path = "examples/pattern.rs" - -[[example]] -name = "set_menus" -path = "examples/set_menus.rs" - -[[example]] -name = "shadow" -path = "examples/shadow.rs" - -[[example]] -name = "svg" -path = "examples/svg/svg.rs" - -[[example]] -name = "tab_stop" -path = "examples/tab_stop.rs" - -[[example]] -name = "text" -path = "examples/text.rs" - -[[example]] -name = "text_wrapper" -path = "examples/text_wrapper.rs" - -[[example]] -name = "tree" -path = "examples/tree.rs" - -[[example]] -name = "uniform_list" -path = "examples/uniform_list.rs" - -[[example]] -name = "window_shadow" -path = "examples/window_shadow.rs" - -[[example]] -name = "grid_layout" -path = "examples/grid_layout.rs" diff --git a/crates/gpui/LICENSE-APACHE b/crates/gpui/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/gpui/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/gpui/README.md b/crates/gpui/README.md deleted file mode 100644 index ad3fd37fc5..0000000000 --- a/crates/gpui/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Welcome to GPUI! - -GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework -for Rust, designed to support a wide variety of applications. - -## Getting Started - -GPUI is still in active development as we work on the Zed code editor, and is still pre-1.0. There will often be breaking changes between versions. You'll also need to use the latest version of stable Rust and be on macOS or Linux. Add the following to your `Cargo.toml`: - -```toml -gpui = { version = "*" } -``` - - - [Ownership and data flow](_ownership_and_data_flow) - -Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example. - -### Dependencies - -GPUI has various system dependencies that it needs in order to work. - -#### macOS - -On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following: - -- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account. - -> Ensure you launch Xcode after installing, and install the macOS components, which is the default option. - -- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/) - - ```sh - xcode-select --install - ``` - -- Ensure that the Xcode command line tools are using your newly installed copy of Xcode: - - ```sh - sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer - ``` - -## The Big Picture - -GPUI offers three different [registers]() depending on your needs: - -- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information. - -- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering. - -- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information. - -Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework. - -## Other Resources - -In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications: - -- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information. - -- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`. - -- An async executor that is integrated with the platform's event loop. See the `executor` module for more information., - -- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details. - -Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog). diff --git a/crates/gpui/examples/animation.rs b/crates/gpui/examples/animation.rs deleted file mode 100644 index 16d6e1b269..0000000000 --- a/crates/gpui/examples/animation.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::time::Duration; - -use anyhow::Result; -use gpui::{ - Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, SharedString, - Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage, - prelude::*, px, size, svg, -}; - -struct Assets {} - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result>> { - std::fs::read(path) - .map(Into::into) - .map_err(Into::into) - .map(Some) - } - - fn list(&self, path: &str) -> Result> { - Ok(std::fs::read_dir(path)? - .filter_map(|entry| { - Some(SharedString::from( - entry.ok()?.path().to_string_lossy().into_owned(), - )) - }) - .collect::>()) - } -} - -const ARROW_CIRCLE_SVG: &str = concat!( - env!("CARGO_MANIFEST_DIR"), - "/examples/image/arrow_circle.svg" -); - -struct AnimationExample {} - -impl Render for AnimationExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .size_full() - .bg(gpui::white()) - .text_color(gpui::black()) - .justify_around() - .child( - div() - .flex() - .flex_col() - .size_full() - .justify_around() - .child( - div() - .id("content") - .flex() - .flex_col() - .h(px(150.)) - .overflow_y_scroll() - .w_full() - .flex_1() - .justify_center() - .items_center() - .text_xl() - .gap_4() - .child("Hello Animation") - .child( - svg() - .size_20() - .overflow_hidden() - .path(ARROW_CIRCLE_SVG) - .text_color(gpui::black()) - .with_animation( - "image_circle", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(bounce(ease_in_out)), - |svg, delta| { - svg.with_transformation(Transformation::rotate( - percentage(delta), - )) - }, - ), - ), - ) - .child( - div() - .flex() - .h(px(64.)) - .w_full() - .p_2() - .justify_center() - .items_center() - .border_t_1() - .border_color(gpui::black().opacity(0.1)) - .bg(gpui::black().opacity(0.05)) - .child("Other Panel"), - ), - ) - } -} - -fn main() { - Application::new() - .with_assets(Assets {}) - .run(|cx: &mut App| { - let options = WindowOptions { - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(300.), px(300.)), - cx, - ))), - ..Default::default() - }; - cx.open_window(options, |_, cx| { - cx.activate(false); - cx.new(|_| AnimationExample {}) - }) - .unwrap(); - }); -} diff --git a/crates/gpui/examples/drag_drop.rs b/crates/gpui/examples/drag_drop.rs deleted file mode 100644 index cf5048f6d3..0000000000 --- a/crates/gpui/examples/drag_drop.rs +++ /dev/null @@ -1,137 +0,0 @@ -use gpui::{ - App, Application, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds, - WindowOptions, div, prelude::*, px, rgb, size, -}; - -#[derive(Clone, Copy)] -struct DragInfo { - ix: usize, - color: Hsla, - position: Point, -} - -impl DragInfo { - fn new(ix: usize, color: Hsla) -> Self { - Self { - ix, - color, - position: Point::default(), - } - } - - fn position(mut self, pos: Point) -> Self { - self.position = pos; - self - } -} - -impl Render for DragInfo { - fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { - let size = gpui::size(px(120.), px(50.)); - - div() - .pl(self.position.x - size.width.half()) - .pt(self.position.y - size.height.half()) - .child( - div() - .flex() - .justify_center() - .items_center() - .w(size.width) - .h(size.height) - .bg(self.color.opacity(0.5)) - .text_color(gpui::white()) - .text_xs() - .shadow_md() - .child(format!("Item {}", self.ix)), - ) - } -} - -struct DragDrop { - drop_on: Option, -} - -impl DragDrop { - fn new() -> Self { - Self { drop_on: None } - } -} - -impl Render for DragDrop { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let items = [gpui::blue(), gpui::red(), gpui::green()]; - - div() - .size_full() - .flex() - .flex_col() - .gap_5() - .bg(gpui::white()) - .justify_center() - .items_center() - .text_color(rgb(0x333333)) - .child(div().text_xl().text_center().child("Drop & Drop")) - .child( - div() - .w_full() - .mb_10() - .justify_center() - .flex() - .flex_row() - .gap_4() - .items_center() - .children(items.into_iter().enumerate().map(|(ix, color)| { - let drag_info = DragInfo::new(ix, color); - - div() - .id(("item", ix)) - .size_32() - .flex() - .justify_center() - .items_center() - .border_2() - .border_color(color) - .text_color(color) - .cursor_move() - .hover(|this| this.bg(color.opacity(0.2))) - .child(format!("Item ({})", ix)) - .on_drag(drag_info, |info: &DragInfo, position, _, cx| { - cx.new(|_| info.position(position)) - }) - })), - ) - .child( - div() - .id("drop-target") - .w_128() - .h_32() - .flex() - .justify_center() - .items_center() - .border_3() - .border_color(self.drop_on.map(|info| info.color).unwrap_or(gpui::black())) - .when_some(self.drop_on, |this, info| this.bg(info.color.opacity(0.5))) - .on_drop(cx.listener(|this, info: &DragInfo, _, _| { - this.drop_on = Some(*info); - })) - .child("Drop items here"), - ) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| DragDrop::new()), - ) - .unwrap(); - - cx.activate(true); - }); -} diff --git a/crates/gpui/examples/grid_layout.rs b/crates/gpui/examples/grid_layout.rs deleted file mode 100644 index f285497578..0000000000 --- a/crates/gpui/examples/grid_layout.rs +++ /dev/null @@ -1,80 +0,0 @@ -use gpui::{ - App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*, - px, rgb, size, -}; - -// https://en.wikipedia.org/wiki/Holy_grail_(web_design) -struct HolyGrailExample {} - -impl Render for HolyGrailExample { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let block = |color: Hsla| { - div() - .size_full() - .bg(color) - .border_1() - .border_dashed() - .rounded_md() - .border_color(gpui::white()) - .items_center() - }; - - div() - .gap_1() - .grid() - .bg(rgb(0x505050)) - .size(px(500.0)) - .shadow_lg() - .border_1() - .size_full() - .grid_cols(5) - .grid_rows(5) - .child( - block(gpui::white()) - .row_span(1) - .col_span_full() - .child("Header"), - ) - .child( - block(gpui::red()) - .col_span(1) - .h_56() - .child("Table of contents"), - ) - .child( - block(gpui::green()) - .col_span(3) - .row_span(3) - .child("Content"), - ) - .child( - block(gpui::blue()) - .col_span(1) - .row_span(3) - .child("AD :(") - .text_color(gpui::white()), - ) - .child( - block(gpui::black()) - .row_span(1) - .col_span_full() - .text_color(gpui::white()) - .child("Footer"), - ) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| HolyGrailExample {}), - ) - .unwrap(); - cx.activate(true); - }); -} diff --git a/crates/gpui/examples/image_gallery.rs b/crates/gpui/examples/image_gallery.rs deleted file mode 100644 index 1fa7a8678f..0000000000 --- a/crates/gpui/examples/image_gallery.rs +++ /dev/null @@ -1,288 +0,0 @@ -use futures::FutureExt; -use gpui::{ - App, AppContext, Application, Asset as _, AssetLogger, Bounds, ClickEvent, Context, ElementId, - Entity, ImageAssetLoader, ImageCache, ImageCacheProvider, KeyBinding, Menu, MenuItem, - RetainAllImageCache, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions, - actions, div, hash, image_cache, img, prelude::*, px, rgb, size, -}; -use reqwest_client::ReqwestClient; -use std::{collections::HashMap, sync::Arc}; - -const IMAGES_IN_GALLERY: usize = 30; - -struct ImageGallery { - image_key: String, - items_count: usize, - total_count: usize, - image_cache: Entity, -} - -impl ImageGallery { - fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.image_cache - .update(cx, |image_cache, cx| image_cache.clear(window, cx)); - - let t = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_millis(); - - self.image_key = format!("{}", t); - self.total_count += self.items_count; - cx.notify(); - } -} - -impl Render for ImageGallery { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let image_url: SharedString = - format!("https://picsum.photos/400/200?t={}", self.image_key).into(); - - div() - .flex() - .flex_col() - .text_color(gpui::white()) - .child("Manually managed image cache:") - .child( - div() - .image_cache(self.image_cache.clone()) - .id("main") - .text_color(gpui::black()) - .bg(rgb(0xE9E9E9)) - .overflow_y_scroll() - .p_4() - .size_full() - .flex() - .flex_col() - .items_center() - .gap_2() - .child( - div() - .w_full() - .flex() - .flex_row() - .justify_between() - .child(format!( - "Example to show images and test memory usage (Rendered: {} images).", - self.total_count - )) - .child( - div() - .id("btn") - .py_1() - .px_4() - .bg(gpui::black()) - .hover(|this| this.opacity(0.8)) - .text_color(gpui::white()) - .text_center() - .w_40() - .child("Next Photos") - .on_click(cx.listener(Self::on_next_image)), - ), - ) - .child( - div() - .id("image-gallery") - .flex() - .flex_row() - .flex_wrap() - .gap_x_4() - .gap_y_2() - .justify_around() - .children( - (0..self.items_count) - .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()), - ), - ), - ) - .child( - "Automatically managed image cache:" - ) - .child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child( - div() - .id("main") - .bg(rgb(0xE9E9E9)) - .text_color(gpui::black()) - .overflow_y_scroll() - .p_4() - .size_full() - .flex() - .flex_col() - .items_center() - .gap_2() - .child( - div() - .id("image-gallery") - .flex() - .flex_row() - .flex_wrap() - .gap_x_4() - .gap_y_2() - .justify_around() - .children( - (0..self.items_count) - .map(|ix| img(format!("{}-{}", image_url, ix)).size_20()), - ), - ) - )) - } -} - -fn simple_lru_cache(id: impl Into, max_items: usize) -> SimpleLruCacheProvider { - SimpleLruCacheProvider { - id: id.into(), - max_items, - } -} - -struct SimpleLruCacheProvider { - id: ElementId, - max_items: usize, -} - -impl ImageCacheProvider for SimpleLruCacheProvider { - fn provide(&mut self, window: &mut Window, cx: &mut App) -> gpui::AnyImageCache { - window - .with_global_id(self.id.clone(), |global_id, window| { - window.with_element_state::, _>( - global_id, - |lru_cache, _window| { - let mut lru_cache = lru_cache.unwrap_or_else(|| { - cx.new(|cx| SimpleLruCache::new(self.max_items, cx)) - }); - if lru_cache.read(cx).max_items != self.max_items { - lru_cache = cx.new(|cx| SimpleLruCache::new(self.max_items, cx)); - } - (lru_cache.clone(), lru_cache) - }, - ) - }) - .into() - } -} - -struct SimpleLruCache { - max_items: usize, - usages: Vec, - cache: HashMap, -} - -impl SimpleLruCache { - fn new(max_items: usize, cx: &mut Context) -> Self { - cx.on_release(|simple_cache, cx| { - for (_, mut item) in std::mem::take(&mut simple_cache.cache) { - if let Some(Ok(image)) = item.get() { - cx.drop_image(image, None); - } - } - }) - .detach(); - - Self { - max_items, - usages: Vec::with_capacity(max_items), - cache: HashMap::with_capacity(max_items), - } - } -} - -impl ImageCache for SimpleLruCache { - fn load( - &mut self, - resource: &gpui::Resource, - window: &mut Window, - cx: &mut App, - ) -> Option, gpui::ImageCacheError>> { - assert_eq!(self.usages.len(), self.cache.len()); - assert!(self.cache.len() <= self.max_items); - - let hash = hash(resource); - - if let Some(item) = self.cache.get_mut(&hash) { - let current_ix = self - .usages - .iter() - .position(|item| *item == hash) - .expect("cache and usages must stay in sync"); - self.usages.remove(current_ix); - self.usages.insert(0, hash); - - return item.get(); - } - - let fut = AssetLogger::::load(resource.clone(), cx); - let task = cx.background_executor().spawn(fut).shared(); - if self.usages.len() == self.max_items { - let oldest = self.usages.pop().unwrap(); - let mut image = self - .cache - .remove(&oldest) - .expect("cache and usages must be in sync"); - if let Some(Ok(image)) = image.get() { - cx.drop_image(image, Some(window)); - } - } - self.cache - .insert(hash, gpui::ImageCacheItem::Loading(task.clone())); - self.usages.insert(0, hash); - - let entity = window.current_view(); - window - .spawn(cx, { - async move |cx| { - _ = task.await; - cx.on_next_frame(move |_, cx| { - cx.notify(entity); - }); - } - }) - .detach(); - - None - } -} - -actions!(image, [Quit]); - -fn main() { - env_logger::init(); - - Application::new().run(move |cx: &mut App| { - let http_client = ReqwestClient::user_agent("gpui example").unwrap(); - cx.set_http_client(Arc::new(http_client)); - - cx.activate(true); - cx.on_action(|_: &Quit, cx| cx.quit()); - cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - cx.set_menus(vec![Menu { - name: "Image Gallery".into(), - items: vec![MenuItem::action("Quit", Quit)], - }]); - - let window_options = WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some(SharedString::from("Image Gallery")), - appears_transparent: false, - ..Default::default() - }), - - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(px(1100.), px(860.)), - cx, - ))), - - ..Default::default() - }; - - cx.open_window(window_options, |_, cx| { - cx.new(|ctx| ImageGallery { - image_key: "".into(), - items_count: IMAGES_IN_GALLERY, - total_count: 0, - image_cache: RetainAllImageCache::new(ctx), - }) - }) - .unwrap(); - }); -} diff --git a/crates/gpui/examples/ownership_post.rs b/crates/gpui/examples/ownership_post.rs deleted file mode 100644 index e690de54a8..0000000000 --- a/crates/gpui/examples/ownership_post.rs +++ /dev/null @@ -1,35 +0,0 @@ -use gpui::{App, Application, Context, Entity, EventEmitter, prelude::*}; - -struct Counter { - count: usize, -} - -struct Change { - increment: usize, -} - -impl EventEmitter for Counter {} - -fn main() { - Application::new().run(|cx: &mut App| { - let counter: Entity = cx.new(|_cx| Counter { count: 0 }); - let subscriber = cx.new(|cx: &mut Context| { - cx.subscribe(&counter, |subscriber, _emitter, event, _cx| { - subscriber.count += event.increment * 2; - }) - .detach(); - - Counter { - count: counter.read(cx).count * 2, - } - }); - - counter.update(cx, |counter, cx| { - counter.count += 2; - cx.notify(); - cx.emit(Change { increment: 2 }); - }); - - assert_eq!(subscriber.read(cx).count, 4); - }); -} diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs deleted file mode 100644 index 9f15d12f46..0000000000 --- a/crates/gpui/examples/painting.rs +++ /dev/null @@ -1,463 +0,0 @@ -use gpui::{ - Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, - PathStyle, Pixels, Point, Render, StrokeOptions, Window, WindowOptions, canvas, div, - linear_color_stop, linear_gradient, point, prelude::*, px, quad, rgb, size, -}; - -struct PaintingViewer { - default_lines: Vec<(Path, Background)>, - background_quads: Vec<(Bounds, Background)>, - lines: Vec>>, - start: Point, - dashed: bool, - _painting: bool, -} - -impl PaintingViewer { - fn new(_window: &mut Window, _cx: &mut Context) -> Self { - let mut lines = vec![]; - - // Black squares beneath transparent paths. - let background_quads = vec![ - ( - Bounds { - origin: point(px(70.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(170.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(270.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(370.), px(70.)), - size: size(px(40.), px(40.)), - }, - gpui::black().into(), - ), - ( - Bounds { - origin: point(px(450.), px(50.)), - size: size(px(80.), px(80.)), - }, - gpui::black().into(), - ), - ]; - - // 50% opaque red path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(50.), px(50.))); - builder.line_to(point(px(130.), px(50.))); - builder.line_to(point(px(130.), px(130.))); - builder.line_to(point(px(50.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let mut red = rgb(0xFF0000); - red.a = 0.5; - lines.push((path, red.into())); - - // 50% opaque blue path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(150.), px(50.))); - builder.line_to(point(px(230.), px(50.))); - builder.line_to(point(px(230.), px(130.))); - builder.line_to(point(px(150.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let mut blue = rgb(0x0000FF); - blue.a = 0.5; - lines.push((path, blue.into())); - - // 50% opaque green path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(250.), px(50.))); - builder.line_to(point(px(330.), px(50.))); - builder.line_to(point(px(330.), px(130.))); - builder.line_to(point(px(250.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let mut green = rgb(0x00FF00); - green.a = 0.5; - lines.push((path, green.into())); - - // 50% opaque black path that extends across black quad. - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(350.), px(50.))); - builder.line_to(point(px(430.), px(50.))); - builder.line_to(point(px(430.), px(130.))); - builder.line_to(point(px(350.), px(130.))); - builder.close(); - let path = builder.build().unwrap(); - let mut black = rgb(0x000000); - black.a = 0.5; - lines.push((path, black.into())); - - // Two 50% opaque red circles overlapping - center should be darker red - let mut builder = PathBuilder::fill(); - let center = point(px(530.), px(85.)); - let radius = px(30.); - builder.move_to(point(center.x + radius, center.y)); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x - radius, center.y), - ); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x + radius, center.y), - ); - builder.close(); - let path = builder.build().unwrap(); - let mut red1 = rgb(0xFF0000); - red1.a = 0.5; - lines.push((path, red1.into())); - - let mut builder = PathBuilder::fill(); - let center = point(px(570.), px(85.)); - let radius = px(30.); - builder.move_to(point(center.x + radius, center.y)); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x - radius, center.y), - ); - builder.arc_to( - point(radius, radius), - px(0.), - false, - false, - point(center.x + radius, center.y), - ); - builder.close(); - let path = builder.build().unwrap(); - let mut red2 = rgb(0xFF0000); - red2.a = 0.5; - lines.push((path, red2.into())); - - // draw a Rust logo - let mut builder = lyon::path::Path::svg_builder(); - lyon::extra::rust_logo::build_logo_path(&mut builder); - // move down the Path - let mut builder: PathBuilder = builder.into(); - builder.translate(point(px(10.), px(200.))); - builder.scale(0.9); - let path = builder.build().unwrap(); - lines.push((path, gpui::black().into())); - - // draw a lightening bolt ⚡ - let mut builder = PathBuilder::fill(); - builder.add_polygon( - &[ - point(px(150.), px(300.)), - point(px(200.), px(225.)), - point(px(200.), px(275.)), - point(px(250.), px(200.)), - ], - false, - ); - let path = builder.build().unwrap(); - lines.push((path, rgb(0x1d4ed8).into())); - - // draw a ⭐ - let mut builder = PathBuilder::fill(); - builder.move_to(point(px(350.), px(200.))); - builder.line_to(point(px(370.), px(260.))); - builder.line_to(point(px(430.), px(260.))); - builder.line_to(point(px(380.), px(300.))); - builder.line_to(point(px(400.), px(360.))); - builder.line_to(point(px(350.), px(320.))); - builder.line_to(point(px(300.), px(360.))); - builder.line_to(point(px(320.), px(300.))); - builder.line_to(point(px(270.), px(260.))); - builder.line_to(point(px(330.), px(260.))); - builder.line_to(point(px(350.), px(200.))); - let path = builder.build().unwrap(); - lines.push(( - path, - linear_gradient( - 180., - linear_color_stop(rgb(0xFACC15), 0.7), - linear_color_stop(rgb(0xD56D0C), 1.), - ) - .color_space(ColorSpace::Oklab), - )); - - // draw linear gradient - let square_bounds = Bounds { - origin: point(px(450.), px(200.)), - size: size(px(200.), px(80.)), - }; - let height = square_bounds.size.height; - let horizontal_offset = height; - let vertical_offset = px(30.); - let mut builder = PathBuilder::fill(); - builder.move_to(square_bounds.bottom_left()); - builder.curve_to( - square_bounds.origin + point(horizontal_offset, vertical_offset), - square_bounds.origin + point(px(0.0), vertical_offset), - ); - builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset)); - builder.curve_to( - square_bounds.bottom_right(), - square_bounds.top_right() + point(px(0.0), vertical_offset), - ); - builder.line_to(square_bounds.bottom_left()); - let path = builder.build().unwrap(); - lines.push(( - path, - linear_gradient( - 180., - linear_color_stop(gpui::blue(), 0.4), - linear_color_stop(gpui::red(), 1.), - ), - )); - - // draw a pie chart - let center = point(px(96.), px(96.)); - let pie_center = point(px(775.), px(255.)); - let segments = [ - ( - point(px(871.), px(255.)), - point(px(747.), px(163.)), - rgb(0x1374e9), - ), - ( - point(px(747.), px(163.)), - point(px(679.), px(263.)), - rgb(0xe13527), - ), - ( - point(px(679.), px(263.)), - point(px(754.), px(349.)), - rgb(0x0751ce), - ), - ( - point(px(754.), px(349.)), - point(px(854.), px(310.)), - rgb(0x209742), - ), - ( - point(px(854.), px(310.)), - point(px(871.), px(255.)), - rgb(0xfbc10a), - ), - ]; - - for (start, end, color) in segments { - let mut builder = PathBuilder::fill(); - builder.move_to(start); - builder.arc_to(center, px(0.), false, false, end); - builder.line_to(pie_center); - builder.close(); - let path = builder.build().unwrap(); - lines.push((path, color.into())); - } - - // draw a wave - let options = StrokeOptions::default() - .with_line_width(1.) - .with_line_join(lyon::path::LineJoin::Bevel); - let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options)); - builder.move_to(point(px(40.), px(420.))); - for i in 1..50 { - builder.line_to(point( - px(40.0 + i as f32 * 10.0), - px(420.0 + (i as f32 * 10.0).sin() * 40.0), - )); - } - let path = builder.build().unwrap(); - lines.push((path, gpui::green().into())); - - Self { - default_lines: lines.clone(), - background_quads, - lines: vec![], - start: point(px(0.), px(0.)), - dashed: false, - _painting: false, - } - } - - fn clear(&mut self, cx: &mut Context) { - self.lines.clear(); - cx.notify(); - } -} - -fn button( - text: &str, - cx: &mut Context, - on_click: impl Fn(&mut PaintingViewer, &mut Context) + 'static, -) -> impl IntoElement { - div() - .id(text.to_string()) - .child(text.to_string()) - .bg(gpui::black()) - .text_color(gpui::white()) - .active(|this| this.opacity(0.8)) - .flex() - .px_3() - .py_1() - .on_click(cx.listener(move |this, _, _, cx| on_click(this, cx))) -} - -impl Render for PaintingViewer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let default_lines = self.default_lines.clone(); - let background_quads = self.background_quads.clone(); - let lines = self.lines.clone(); - let dashed = self.dashed; - - div() - .bg(gpui::white()) - .size_full() - .p_4() - .flex() - .flex_col() - .child( - div() - .flex() - .gap_2() - .justify_between() - .items_center() - .child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)") - .child( - div() - .flex() - .gap_x_2() - .child(button( - if dashed { "Solid" } else { "Dashed" }, - cx, - move |this, _| this.dashed = !dashed, - )) - .child(button("Clear", cx, |this, cx| this.clear(cx))), - ), - ) - .child( - div() - .size_full() - .child( - canvas( - move |_, _, _| {}, - move |_, _, window, _| { - // First draw background quads - for (bounds, color) in background_quads.iter() { - window.paint_quad(quad( - *bounds, - px(0.), - *color, - px(0.), - gpui::transparent_black(), - Default::default(), - )); - } - - // Then draw the default paths on top - for (path, color) in default_lines { - window.paint_path(path, color); - } - - for points in lines { - if points.len() < 2 { - continue; - } - - let mut builder = PathBuilder::stroke(px(1.)); - if dashed { - builder = builder.dash_array(&[px(4.), px(2.)]); - } - for (i, p) in points.into_iter().enumerate() { - if i == 0 { - builder.move_to(p); - } else { - builder.line_to(p); - } - } - - if let Ok(path) = builder.build() { - window.paint_path(path, gpui::black()); - } - } - }, - ) - .size_full(), - ) - .on_mouse_down( - gpui::MouseButton::Left, - cx.listener(|this, ev: &MouseDownEvent, _, _| { - this._painting = true; - this.start = ev.position; - let path = vec![ev.position]; - this.lines.push(path); - }), - ) - .on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| { - if !this._painting { - return; - } - - let is_shifted = ev.modifiers.shift; - let mut pos = ev.position; - // When holding shift, draw a straight line - if is_shifted { - let dx = pos.x - this.start.x; - let dy = pos.y - this.start.y; - if dx.abs() > dy.abs() { - pos.y = this.start.y; - } else { - pos.x = this.start.x; - } - } - - if let Some(path) = this.lines.last_mut() { - path.push(pos); - } - - cx.notify(); - })) - .on_mouse_up( - gpui::MouseButton::Left, - cx.listener(|this, _, _, _| { - this._painting = false; - }), - ), - ) - } -} - -fn main() { - Application::new().run(|cx| { - cx.open_window( - WindowOptions { - focus: true, - ..Default::default() - }, - |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), - ) - .unwrap(); - cx.on_window_closed(|cx| { - cx.quit(); - }) - .detach(); - cx.activate(true); - }); -} diff --git a/crates/gpui/examples/set_menus.rs b/crates/gpui/examples/set_menus.rs deleted file mode 100644 index f4e5e5e112..0000000000 --- a/crates/gpui/examples/set_menus.rs +++ /dev/null @@ -1,107 +0,0 @@ -use gpui::{ - App, Application, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window, - WindowOptions, actions, div, prelude::*, rgb, -}; - -struct SetMenus; - -impl Render for SetMenus { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .bg(rgb(0x2e7d32)) - .size_full() - .justify_center() - .items_center() - .text_xl() - .text_color(rgb(0xffffff)) - .child("Set Menus Example") - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - cx.set_global(AppState::new()); - - // Bring the menu bar to the foreground (so you can see the menu bar) - cx.activate(true); - // Register the `quit` function so it can be referenced by the `MenuItem::action` in the menu bar - cx.on_action(quit); - cx.on_action(toggle_check); - // Add menu items - set_app_menus(cx); - cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {})) - .unwrap(); - }); -} - -#[derive(PartialEq)] -enum ViewMode { - List, - Grid, -} - -impl ViewMode { - fn toggle(&mut self) { - *self = match self { - ViewMode::List => ViewMode::Grid, - ViewMode::Grid => ViewMode::List, - } - } -} - -impl Into for ViewMode { - fn into(self) -> SharedString { - match self { - ViewMode::List => "List", - ViewMode::Grid => "Grid", - } - .into() - } -} - -struct AppState { - view_mode: ViewMode, -} - -impl AppState { - fn new() -> Self { - Self { - view_mode: ViewMode::List, - } - } -} - -impl Global for AppState {} - -fn set_app_menus(cx: &mut App) { - let app_state = cx.global::(); - cx.set_menus(vec![Menu { - name: "set_menus".into(), - items: vec![ - MenuItem::os_submenu("Services", SystemMenuType::Services), - MenuItem::separator(), - MenuItem::action(ViewMode::List, ToggleCheck) - .checked(app_state.view_mode == ViewMode::List), - MenuItem::action(ViewMode::Grid, ToggleCheck) - .checked(app_state.view_mode == ViewMode::Grid), - MenuItem::separator(), - MenuItem::action("Quit", Quit), - ], - }]); -} - -// Associate actions using the `actions!` macro (or `Action` derive macro) -actions!(set_menus, [Quit, ToggleCheck]); - -// Define the quit function that is registered with the App -fn quit(_: &Quit, cx: &mut App) { - println!("Gracefully quitting the application . . ."); - cx.quit(); -} - -fn toggle_check(_: &ToggleCheck, cx: &mut App) { - let app_state = cx.global_mut::(); - app_state.view_mode.toggle(); - set_app_menus(cx); -} diff --git a/crates/gpui/examples/text.rs b/crates/gpui/examples/text.rs deleted file mode 100644 index 3cb95897b6..0000000000 --- a/crates/gpui/examples/text.rs +++ /dev/null @@ -1,333 +0,0 @@ -use std::{ - ops::{Deref, DerefMut}, - sync::Arc, -}; - -use gpui::{ - AbsoluteLength, App, Application, Context, DefiniteLength, ElementId, Global, Hsla, Menu, - SharedString, TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds, - colors::DefaultColors, div, point, prelude::*, px, relative, rgb, size, -}; -use std::iter; - -#[derive(Clone, Debug)] -pub struct TextContext { - font_size: f32, - line_height: f32, - type_scale: f32, -} - -impl Default for TextContext { - fn default() -> Self { - TextContext { - font_size: 16.0, - line_height: 1.3, - type_scale: 1.33, - } - } -} - -impl TextContext { - pub fn get_global(cx: &App) -> &Arc { - &cx.global::().0 - } -} - -#[derive(Clone, Debug)] -pub struct GlobalTextContext(pub Arc); - -impl Deref for GlobalTextContext { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for GlobalTextContext { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Global for GlobalTextContext {} - -pub trait ActiveTextContext { - fn text_context(&self) -> &Arc; -} - -impl ActiveTextContext for App { - fn text_context(&self) -> &Arc { - &self.global::().0 - } -} - -#[derive(Clone, PartialEq)] -pub struct SpecimenTheme { - pub bg: Hsla, - pub fg: Hsla, -} - -impl Default for SpecimenTheme { - fn default() -> Self { - Self { - bg: gpui::white(), - fg: gpui::black(), - } - } -} - -impl SpecimenTheme { - pub fn invert(&self) -> Self { - Self { - bg: self.fg, - fg: self.bg, - } - } -} - -#[derive(Debug, Clone, PartialEq, IntoElement)] -struct Specimen { - id: ElementId, - scale: f32, - text_style: Option, - string: SharedString, - invert: bool, -} - -impl Specimen { - pub fn new(id: usize) -> Self { - let string = SharedString::new_static("The quick brown fox jumps over the lazy dog"); - let id_string = format!("specimen-{}", id); - let id = ElementId::Name(id_string.into()); - Self { - id, - scale: 1.0, - text_style: None, - string, - invert: false, - } - } - - pub fn invert(mut self) -> Self { - self.invert = !self.invert; - self - } - - pub fn scale(mut self, scale: f32) -> Self { - self.scale = scale; - self - } -} - -impl RenderOnce for Specimen { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let rem_size = window.rem_size(); - let scale = self.scale; - let global_style = cx.text_context(); - - let style_override = self.text_style; - - let mut font_size = global_style.font_size; - let mut line_height = global_style.line_height; - - if let Some(style_override) = style_override { - font_size = style_override.font_size.to_pixels(rem_size).into(); - line_height = match style_override.line_height { - DefiniteLength::Absolute(absolute_len) => match absolute_len { - AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).into(), - AbsoluteLength::Pixels(absolute_len) => absolute_len.into(), - }, - DefiniteLength::Fraction(value) => value, - }; - } - - let mut theme = SpecimenTheme::default(); - - if self.invert { - theme = theme.invert(); - } - - div() - .id(self.id) - .bg(theme.bg) - .text_color(theme.fg) - .text_size(px(font_size * scale)) - .line_height(relative(line_height)) - .p(px(10.0)) - .child(self.string) - } -} - -#[derive(Debug, Clone, PartialEq, IntoElement)] -struct CharacterGrid { - scale: f32, - invert: bool, - text_style: Option, -} - -impl CharacterGrid { - pub fn new() -> Self { - Self { - scale: 1.0, - invert: false, - text_style: None, - } - } - - pub fn scale(mut self, scale: f32) -> Self { - self.scale = scale; - self - } -} - -impl RenderOnce for CharacterGrid { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let mut theme = SpecimenTheme::default(); - - if self.invert { - theme = theme.invert(); - } - - let characters = vec![ - "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G", - "H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", - "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q", - "r", "s", "t", "u", "v", "w", "x", "y", "z", "ẞ", "ſ", "ß", "ð", "Þ", "þ", "α", "β", - "Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ", - "χ", "ψ", "∂", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р", - "У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*", - "_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "¶", "µ", - "❮", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎", - ]; - - let columns = 11; - let rows = characters.len().div_ceil(columns); - - let grid_rows = (0..rows).map(|row_idx| { - let start_idx = row_idx * columns; - let end_idx = (start_idx + columns).min(characters.len()); - - div() - .w_full() - .flex() - .flex_row() - .children((start_idx..end_idx).map(|i| { - div() - .text_center() - .size(px(62.)) - .bg(theme.bg) - .text_color(theme.fg) - .text_size(px(24.0)) - .line_height(relative(1.0)) - .child(characters[i]) - })) - .when(end_idx - start_idx < columns, |d| { - d.children( - iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)), - ) - }) - }); - - div().p_4().gap_2().flex().flex_col().children(grid_rows) - } -} - -struct TextExample { - next_id: usize, -} - -impl TextExample { - fn next_id(&mut self) -> usize { - self.next_id += 1; - self.next_id - } -} - -impl Render for TextExample { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let tcx = cx.text_context(); - let colors = cx.default_colors().clone(); - - let type_scale = tcx.type_scale; - - let step_down_2 = 1.0 / (type_scale * type_scale); - let step_down_1 = 1.0 / type_scale; - let base = 1.0; - let step_up_1 = base * type_scale; - let step_up_2 = step_up_1 * type_scale; - let step_up_3 = step_up_2 * type_scale; - let step_up_4 = step_up_3 * type_scale; - let step_up_5 = step_up_4 * type_scale; - let step_up_6 = step_up_5 * type_scale; - - div() - .size_full() - .child( - div() - .id("text-example") - .overflow_y_scroll() - .overflow_x_hidden() - .bg(rgb(0xffffff)) - .size_full() - .child(div().child(CharacterGrid::new().scale(base))) - .child( - div() - .child(Specimen::new(self.next_id()).scale(step_down_2)) - .child(Specimen::new(self.next_id()).scale(step_down_2).invert()) - .child(Specimen::new(self.next_id()).scale(step_down_1)) - .child(Specimen::new(self.next_id()).scale(step_down_1).invert()) - .child(Specimen::new(self.next_id()).scale(base)) - .child(Specimen::new(self.next_id()).scale(base).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_1)) - .child(Specimen::new(self.next_id()).scale(step_up_1).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_2)) - .child(Specimen::new(self.next_id()).scale(step_up_2).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_3)) - .child(Specimen::new(self.next_id()).scale(step_up_3).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_4)) - .child(Specimen::new(self.next_id()).scale(step_up_4).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_5)) - .child(Specimen::new(self.next_id()).scale(step_up_5).invert()) - .child(Specimen::new(self.next_id()).scale(step_up_6)) - .child(Specimen::new(self.next_id()).scale(step_up_6).invert()), - ), - ) - .child(div().w(px(240.)).h_full().bg(colors.container)) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - cx.set_menus(vec![Menu { - name: "GPUI Typography".into(), - items: vec![], - }]); - - cx.init_colors(); - cx.set_global(GlobalTextContext(Arc::new(TextContext::default()))); - - let window = cx - .open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("GPUI Typography".into()), - ..Default::default() - }), - window_bounds: Some(WindowBounds::Windowed(bounds( - point(px(0.0), px(0.0)), - size(px(920.), px(720.)), - ))), - ..Default::default() - }, - |_window, cx| cx.new(|_cx| TextExample { next_id: 0 }), - ) - .unwrap(); - - window - .update(cx, |_view, _window, cx| { - cx.activate(true); - }) - .unwrap(); - }); -} diff --git a/crates/gpui/examples/text_layout.rs b/crates/gpui/examples/text_layout.rs deleted file mode 100644 index 8929955ba8..0000000000 --- a/crates/gpui/examples/text_layout.rs +++ /dev/null @@ -1,96 +0,0 @@ -use gpui::{ - App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds, - WindowOptions, div, prelude::*, px, size, -}; - -struct HelloWorld {} - -impl Render for HelloWorld { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .bg(gpui::white()) - .flex() - .flex_col() - .gap_2() - .p_4() - .size_full() - .child(div().child("Text left")) - .child(div().text_center().child("Text center")) - .child(div().text_right().child("Text right")) - .child(div().text_decoration_1().child("Text left (underline)")) - .child( - div() - .text_center() - .text_decoration_1() - .child("Text center (underline)"), - ) - .child( - div() - .text_right() - .text_decoration_1() - .child("Text right (underline)"), - ) - .child(div().line_through().child("Text left (line_through)")) - .child( - div() - .text_center() - .line_through() - .child("Text center (line_through)"), - ) - .child( - div() - .text_right() - .line_through() - .child("Text right (line_through)"), - ) - .child( - div() - .flex() - .gap_2() - .justify_between() - .child( - div() - .w(px(400.)) - .border_1() - .border_color(gpui::blue()) - .p_1() - .whitespace_nowrap() - .overflow_hidden() - .text_center() - .child("A long non-wrapping text align center"), - ) - .child( - div() - .w_32() - .border_1() - .border_color(gpui::blue()) - .p_1() - .whitespace_nowrap() - .overflow_hidden() - .text_right() - .child("100%"), - ), - ) - .child(div().flex().gap_2().justify_between().child( - StyledText::new("ABCD").with_highlights([ - (0..1, FontWeight::EXTRA_BOLD.into()), - (2..3, FontStyle::Italic.into()), - ]), - )) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| HelloWorld {}), - ) - .unwrap(); - cx.activate(true); - }); -} diff --git a/crates/gpui/examples/text_wrapper.rs b/crates/gpui/examples/text_wrapper.rs deleted file mode 100644 index 18372ea9e1..0000000000 --- a/crates/gpui/examples/text_wrapper.rs +++ /dev/null @@ -1,123 +0,0 @@ -use gpui::{ - App, Application, Bounds, Context, TextOverflow, Window, WindowBounds, WindowOptions, div, - prelude::*, px, size, -}; - -struct HelloWorld {} - -impl Render for HelloWorld { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - let text = "The longest word 你好世界这段是中文,こんにちはこの段落は日本語です in any of the major \ - English language dictionaries is pneumonoultramicroscopicsilicovolcanoconiosis, a word that \ - refers to a lung disease contracted from the inhalation of very fine silica particles, \ - a url https://github.com/zed-industries/zed/pull/35724?query=foo&bar=2, \ - specifically from a volcano; medically, it is the same as silicosis."; - div() - .id("page") - .size_full() - .flex() - .flex_col() - .p_2() - .gap_2() - .bg(gpui::white()) - .child( - div() - .flex() - .flex_row() - .flex_shrink_0() - .gap_2() - .child( - div() - .flex() - .border_1() - .border_color(gpui::red()) - .text_ellipsis() - .child("longer text in flex 1"), - ) - .child( - div() - .flex() - .border_1() - .border_color(gpui::red()) - .text_ellipsis() - .child("short flex"), - ) - .child( - div() - .overflow_hidden() - .border_1() - .border_color(gpui::red()) - .text_ellipsis() - .w_full() - .child("A short text in normal div"), - ), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .truncate() - .border_1() - .border_color(gpui::blue()) - .child("ELLIPSIS: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .overflow_hidden() - .text_ellipsis() - .line_clamp(2) - .border_1() - .border_color(gpui::blue()) - .child("ELLIPSIS 2 lines: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .overflow_hidden() - .text_overflow(TextOverflow::Truncate("".into())) - .border_1() - .border_color(gpui::green()) - .child("TRUNCATE: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .overflow_hidden() - .text_overflow(TextOverflow::Truncate("".into())) - .line_clamp(3) - .border_1() - .border_color(gpui::green()) - .child("TRUNCATE 3 lines: ".to_owned() + text), - ) - .child( - div() - .flex_shrink_0() - .text_xl() - .whitespace_nowrap() - .overflow_hidden() - .border_1() - .border_color(gpui::black()) - .child("NOWRAP: ".to_owned() + text), - ) - .child(div().text_xl().w_full().child(text)) - } -} - -fn main() { - Application::new().run(|cx: &mut App| { - let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx); - cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - |_, cx| cx.new(|_| HelloWorld {}), - ) - .unwrap(); - cx.activate(true); - }); -} diff --git a/crates/gpui/src/_ownership_and_data_flow.rs b/crates/gpui/src/_ownership_and_data_flow.rs deleted file mode 100644 index 9bb8bf6695..0000000000 --- a/crates/gpui/src/_ownership_and_data_flow.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities. -//! -//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Entity}; -//! # struct Counter { -//! # count: usize, -//! # } -//! Application::new().run(|cx: &mut App| { -//! let _counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! // ... -//! }); -//! ``` -//! -//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app. -//! -//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity: -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Context, Entity}; -//! # struct Counter { -//! # count: usize, -//! # } -//! Application::new().run(|cx: &mut App| { -//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! // Call `update` to access the model's state. -//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context| { -//! counter.count += 1; -//! }); -//! }); -//! ``` -//! -//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state. -//! -//! The callback is also provided a second `Context` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter. -//! -//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Entity}; -//! # struct Counter { -//! # count: usize, -//! # } -//! Application::new().run(|cx: &mut App| { -//! let counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! counter.update(cx, |counter, cx| { -//! counter.count += 1; -//! cx.notify(); // Notify observers -//! }); -//! }); -//! ``` -//! -//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior. -//! -//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Entity, prelude::*}; -//! # struct Counter { -//! # count: usize, -//! # } -//! Application::new().run(|cx: &mut App| { -//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! -//! let second_counter = cx.new(|cx: &mut Context| { -//! // Note we can set up the callback before the Counter is even created! -//! cx.observe( -//! &first_counter, -//! |second: &mut Counter, first: Entity, cx| { -//! second.count = first.read(cx).count * 2; -//! }, -//! ) -//! .detach(); -//! -//! Counter { count: 0 } -//! }); -//! -//! first_counter.update(cx, |counter, cx| { -//! counter.count += 1; -//! cx.notify(); -//! }); -//! -//! assert_eq!(second_counter.read(cx).count, 2); -//! }); -//! ``` -//! -//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription. -//! -//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait. -//! -//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event: -//! -//! ```no_run -//! use gpui::EventEmitter; -//! # struct Counter { -//! # count: usize, -//! # } -//! struct CounterChangeEvent { -//! increment: usize, -//! } -//! -//! impl EventEmitter for Counter {} -//! ``` -//! -//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase. -//! -//! ```no_run -//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter}; -//! # struct Counter { -//! # count: usize, -//! # } -//! # struct CounterChangeEvent { -//! # increment: usize, -//! # } -//! # impl EventEmitter for Counter {} -//! Application::new().run(|cx: &mut App| { -//! let first_counter: Entity = cx.new(|_cx| Counter { count: 0 }); -//! -//! let second_counter = cx.new(|cx: &mut Context| { -//! // Note we can set up the callback before the Counter is even created! -//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity, event, _cx| { -//! second.count += event.increment * 2; -//! }) -//! .detach(); -//! -//! Counter { -//! count: first_counter.read(cx).count * 2, -//! } -//! }); -//! -//! first_counter.update(cx, |first, cx| { -//! first.count += 2; -//! cx.emit(CounterChangeEvent { increment: 2 }); -//! cx.notify(); -//! }); -//! -//! assert_eq!(second_counter.read(cx).count, 4); -//! }); -//! ``` diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml deleted file mode 100644 index 2ee8da52fb..0000000000 --- a/crates/gpui_macros/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "gpui_macros" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "Macros used by gpui" - -[lints] -workspace = true - -[features] -inspector = [] - -[lib] -path = "src/gpui_macros.rs" -proc-macro = true -doctest = true - -[dependencies] -heck.workspace = true -proc-macro2.workspace = true -quote.workspace = true -syn.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["inspector"] } diff --git a/crates/gpui_macros/LICENSE-APACHE b/crates/gpui_macros/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/gpui_macros/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/gpui_macros/src/derive_action.rs b/crates/gpui_macros/src/derive_action.rs deleted file mode 100644 index 4e6c6277e4..0000000000 --- a/crates/gpui_macros/src/derive_action.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::register_action::generate_register_action; -use proc_macro::TokenStream; -use proc_macro2::Ident; -use quote::quote; -use syn::{Data, DeriveInput, LitStr, Token, parse::ParseStream}; - -pub(crate) fn derive_action(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); - - let struct_name = &input.ident; - let mut name_argument = None; - let mut deprecated_aliases = Vec::new(); - let mut no_json = false; - let mut no_register = false; - let mut namespace = None; - let mut deprecated = None; - let mut doc_str: Option = None; - - /* - * - * #[action()] - * Struct Foo { - * bar: bool // is bar considered an attribute - } - */ - for attr in &input.attrs { - if attr.path().is_ident("action") { - attr.parse_nested_meta(|meta| { - if meta.path.is_ident("name") { - if name_argument.is_some() { - return Err(meta.error("'name' argument specified multiple times")); - } - meta.input.parse::()?; - let lit: LitStr = meta.input.parse()?; - name_argument = Some(lit.value()); - } else if meta.path.is_ident("namespace") { - if namespace.is_some() { - return Err(meta.error("'namespace' argument specified multiple times")); - } - meta.input.parse::()?; - let ident: Ident = meta.input.parse()?; - namespace = Some(ident.to_string()); - } else if meta.path.is_ident("no_json") { - if no_json { - return Err(meta.error("'no_json' argument specified multiple times")); - } - no_json = true; - } else if meta.path.is_ident("no_register") { - if no_register { - return Err(meta.error("'no_register' argument specified multiple times")); - } - no_register = true; - } else if meta.path.is_ident("deprecated_aliases") { - if !deprecated_aliases.is_empty() { - return Err( - meta.error("'deprecated_aliases' argument specified multiple times") - ); - } - meta.input.parse::()?; - // Parse array of string literals - let content; - syn::bracketed!(content in meta.input); - let aliases = content.parse_terminated( - |input: ParseStream| input.parse::(), - Token![,], - )?; - deprecated_aliases.extend(aliases.into_iter().map(|lit| lit.value())); - } else if meta.path.is_ident("deprecated") { - if deprecated.is_some() { - return Err(meta.error("'deprecated' argument specified multiple times")); - } - meta.input.parse::()?; - let lit: LitStr = meta.input.parse()?; - deprecated = Some(lit.value()); - } else { - return Err(meta.error(format!( - "'{:?}' argument not recognized, expected \ - 'namespace', 'no_json', 'no_register, 'deprecated_aliases', or 'deprecated'", - meta.path - ))); - } - Ok(()) - }) - .unwrap_or_else(|e| panic!("in #[action] attribute: {}", e)); - } else if attr.path().is_ident("doc") { - use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue}; - if let Meta::NameValue(MetaNameValue { - value: - Lit(ExprLit { - lit: Str(ref lit_str), - .. - }), - .. - }) = attr.meta - { - let doc = lit_str.value(); - let doc_str = doc_str.get_or_insert_default(); - doc_str.push_str(doc.trim()); - doc_str.push('\n'); - } - } - } - - let name = name_argument.unwrap_or_else(|| struct_name.to_string()); - - if name.contains("::") { - panic!( - "in #[action] attribute: `name = \"{name}\"` must not contain `::`, \ - also specify `namespace` instead" - ); - } - - let full_name = if let Some(namespace) = namespace { - format!("{namespace}::{name}") - } else { - name - }; - - let is_unit_struct = matches!(&input.data, Data::Struct(data) if data.fields.is_empty()); - - let build_fn_body = if no_json { - let error_msg = format!("{} cannot be built from JSON", full_name); - quote! { Err(gpui::private::anyhow::anyhow!(#error_msg)) } - } else if is_unit_struct { - quote! { Ok(Box::new(Self)) } - } else { - quote! { Ok(Box::new(gpui::private::serde_json::from_value::(_value)?)) } - }; - - let json_schema_fn_body = if no_json || is_unit_struct { - quote! { None } - } else { - quote! { Some(::json_schema(_generator)) } - }; - - let deprecated_aliases_fn_body = if deprecated_aliases.is_empty() { - quote! { &[] } - } else { - let aliases = deprecated_aliases.iter(); - quote! { &[#(#aliases),*] } - }; - - let deprecation_fn_body = if let Some(message) = deprecated { - quote! { Some(#message) } - } else { - quote! { None } - }; - - let documentation_fn_body = if let Some(doc) = doc_str { - let doc = doc.trim(); - quote! { Some(#doc) } - } else { - quote! { None } - }; - - let registration = if no_register { - quote! {} - } else { - generate_register_action(struct_name) - }; - - TokenStream::from(quote! { - #registration - - impl gpui::Action for #struct_name { - fn name(&self) -> &'static str { - #full_name - } - - fn name_for_type() -> &'static str - where - Self: Sized - { - #full_name - } - - fn partial_eq(&self, action: &dyn gpui::Action) -> bool { - action - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn build(_value: gpui::private::serde_json::Value) -> gpui::Result> { - #build_fn_body - } - - fn action_json_schema( - _generator: &mut gpui::private::schemars::SchemaGenerator, - ) -> Option { - #json_schema_fn_body - } - - fn deprecated_aliases() -> &'static [&'static str] { - #deprecated_aliases_fn_body - } - - fn deprecation_message() -> Option<&'static str> { - #deprecation_fn_body - } - - fn documentation() -> Option<&'static str> { - #documentation_fn_body - } - } - }) -} diff --git a/crates/gpui_macros/src/derive_app_context.rs b/crates/gpui_macros/src/derive_app_context.rs deleted file mode 100644 index d2dc250d02..0000000000 --- a/crates/gpui_macros/src/derive_app_context.rs +++ /dev/null @@ -1,112 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, parse_macro_input}; - -use crate::get_simple_attribute_field; - -pub fn derive_app_context(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - - let Some(app_variable) = get_simple_attribute_field(&ast, "app") else { - return quote! { - compile_error!("Derive must have an #[app] attribute to detect the &mut App field"); - } - .into(); - }; - - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - - let r#gen = quote! { - impl #impl_generics gpui::AppContext for #type_name #type_generics - #where_clause - { - type Result = T; - - fn new( - &mut self, - build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T, - ) -> Self::Result> { - self.#app_variable.new(build_entity) - } - - fn reserve_entity(&mut self) -> Self::Result> { - self.#app_variable.reserve_entity() - } - - fn insert_entity( - &mut self, - reservation: gpui::Reservation, - build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T, - ) -> Self::Result> { - self.#app_variable.insert_entity(reservation, build_entity) - } - - fn update_entity( - &mut self, - handle: &gpui::Entity, - update: impl FnOnce(&mut T, &mut gpui::Context<'_, T>) -> R, - ) -> Self::Result - where - T: 'static, - { - self.#app_variable.update_entity(handle, update) - } - - fn as_mut<'y, 'z, T>( - &'y mut self, - handle: &'z gpui::Entity, - ) -> Self::Result> - where - T: 'static, - { - self.#app_variable.as_mut(handle) - } - - fn read_entity( - &self, - handle: &gpui::Entity, - read: impl FnOnce(&T, &gpui::App) -> R, - ) -> Self::Result - where - T: 'static, - { - self.#app_variable.read_entity(handle, read) - } - - fn update_window(&mut self, window: gpui::AnyWindowHandle, f: F) -> gpui::Result - where - F: FnOnce(gpui::AnyView, &mut gpui::Window, &mut gpui::App) -> T, - { - self.#app_variable.update_window(window, f) - } - - fn read_window( - &self, - window: &gpui::WindowHandle, - read: impl FnOnce(gpui::Entity, &gpui::App) -> R, - ) -> gpui::Result - where - T: 'static, - { - self.#app_variable.read_window(window, read) - } - - fn background_spawn(&self, future: impl std::future::Future + Send + 'static) -> gpui::Task - where - R: Send + 'static, - { - self.#app_variable.background_spawn(future) - } - - fn read_global(&self, callback: impl FnOnce(&G, &gpui::App) -> R) -> Self::Result - where - G: gpui::Global, - { - self.#app_variable.read_global(callback) - } - } - }; - - r#gen.into() -} diff --git a/crates/gpui_macros/src/derive_inspector_reflection.rs b/crates/gpui_macros/src/derive_inspector_reflection.rs deleted file mode 100644 index 9c1cb503a8..0000000000 --- a/crates/gpui_macros/src/derive_inspector_reflection.rs +++ /dev/null @@ -1,305 +0,0 @@ -//! Implements `#[derive_inspector_reflection]` macro to provide runtime access to trait methods -//! that have the shape `fn method(self) -> Self`. This code was generated using Zed Agent with Claude Opus 4. - -use heck::ToSnakeCase as _; -use proc_macro::TokenStream; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::quote; -use syn::{ - Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type, - parse_macro_input, parse_quote, - visit_mut::{self, VisitMut}, -}; - -pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream { - let mut item = parse_macro_input!(input as Item); - - // First, expand any macros in the trait - match &mut item { - Item::Trait(trait_item) => { - let mut expander = MacroExpander; - expander.visit_item_trait_mut(trait_item); - } - _ => { - return syn::Error::new_spanned( - quote!(#item), - "#[derive_inspector_reflection] can only be applied to traits", - ) - .to_compile_error() - .into(); - } - } - - // Now process the expanded trait - match item { - Item::Trait(trait_item) => generate_reflected_trait(trait_item), - _ => unreachable!(), - } -} - -fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream { - let trait_name = &trait_item.ident; - let vis = &trait_item.vis; - - // Determine if we're being called from within the gpui crate - let call_site = Span::call_site(); - let inspector_reflection_path = if is_called_from_gpui_crate(call_site) { - quote! { crate::inspector_reflection } - } else { - quote! { ::gpui::inspector_reflection } - }; - - // Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self - let mut method_infos = Vec::new(); - - for item in &trait_item.items { - if let TraitItem::Fn(method) = item { - let method_name = &method.sig.ident; - - // Check if method has self or mut self receiver - let has_valid_self_receiver = method - .sig - .inputs - .iter() - .any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none())); - - // Check if method returns Self - let returns_self = match &method.sig.output { - ReturnType::Type(_, ty) => { - matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self")) - } - ReturnType::Default => false, - }; - - // Check if method has exactly one parameter (self or mut self) - let param_count = method.sig.inputs.len(); - - // Include methods of form fn name(self) -> Self or fn name(mut self) -> Self - // This includes methods with default implementations - if has_valid_self_receiver && returns_self && param_count == 1 { - // Extract documentation and cfg attributes - let doc = extract_doc_comment(&method.attrs); - let cfg_attrs = extract_cfg_attributes(&method.attrs); - method_infos.push((method_name.clone(), doc, cfg_attrs)); - } - } - } - - // Generate the reflection module name - let reflection_mod_name = Ident::new( - &format!("{}_reflection", trait_name.to_string().to_snake_case()), - trait_name.span(), - ); - - // Generate wrapper functions for each method - // These wrappers use type erasure to allow runtime invocation - let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| { - let wrapper_name = Ident::new( - &format!("__wrapper_{}", method_name), - method_name.span(), - ); - quote! { - #(#cfg_attrs)* - fn #wrapper_name(value: Box) -> Box { - if let Ok(concrete) = value.downcast::() { - Box::new(concrete.#method_name()) - } else { - panic!("Type mismatch in reflection wrapper"); - } - } - } - }); - - // Generate method info entries - let method_info_entries = method_infos.iter().map(|(method_name, doc, cfg_attrs)| { - let method_name_str = method_name.to_string(); - let wrapper_name = Ident::new(&format!("__wrapper_{}", method_name), method_name.span()); - let doc_expr = match doc { - Some(doc_str) => quote! { Some(#doc_str) }, - None => quote! { None }, - }; - quote! { - #(#cfg_attrs)* - #inspector_reflection_path::FunctionReflection { - name: #method_name_str, - function: #wrapper_name::, - documentation: #doc_expr, - _type: ::std::marker::PhantomData, - } - } - }); - - // Generate the complete output - let output = quote! { - #trait_item - - /// Implements function reflection - #vis mod #reflection_mod_name { - use super::*; - - #(#wrapper_functions)* - - /// Get all reflectable methods for a concrete type implementing the trait - pub fn methods() -> Vec<#inspector_reflection_path::FunctionReflection> { - vec![ - #(#method_info_entries),* - ] - } - - /// Find a method by name for a concrete type implementing the trait - pub fn find_method(name: &str) -> Option<#inspector_reflection_path::FunctionReflection> { - methods::().into_iter().find(|m| m.name == name) - } - } - }; - - TokenStream::from(output) -} - -fn extract_doc_comment(attrs: &[Attribute]) -> Option { - let mut doc_lines = Vec::new(); - - for attr in attrs { - if attr.path().is_ident("doc") - && let Meta::NameValue(meta) = &attr.meta - && let Expr::Lit(expr_lit) = &meta.value - && let Lit::Str(lit_str) = &expr_lit.lit - { - let line = lit_str.value(); - let line = line.strip_prefix(' ').unwrap_or(&line); - doc_lines.push(line.to_string()); - } - } - - if doc_lines.is_empty() { - None - } else { - Some(doc_lines.join("\n")) - } -} - -fn extract_cfg_attributes(attrs: &[Attribute]) -> Vec { - attrs - .iter() - .filter(|attr| attr.path().is_ident("cfg")) - .cloned() - .collect() -} - -fn is_called_from_gpui_crate(_span: Span) -> bool { - // Check if we're being called from within the gpui crate by examining the call site - // This is a heuristic approach - we check if the current crate name is "gpui" - std::env::var("CARGO_PKG_NAME").is_ok_and(|name| name == "gpui") -} - -struct MacroExpander; - -impl VisitMut for MacroExpander { - fn visit_item_trait_mut(&mut self, trait_item: &mut ItemTrait) { - let mut expanded_items = Vec::new(); - let mut items_to_keep = Vec::new(); - - for item in trait_item.items.drain(..) { - match item { - TraitItem::Macro(macro_item) => { - // Try to expand known macros - if let Some(expanded) = try_expand_macro(¯o_item) { - expanded_items.extend(expanded); - } else { - // Keep unknown macros as-is - items_to_keep.push(TraitItem::Macro(macro_item)); - } - } - other => { - items_to_keep.push(other); - } - } - } - - // Rebuild the items list with expanded content first, then original items - trait_item.items = expanded_items; - trait_item.items.extend(items_to_keep); - - // Continue visiting - visit_mut::visit_item_trait_mut(self, trait_item); - } -} - -fn try_expand_macro(macro_item: &syn::TraitItemMacro) -> Option> { - let path = ¯o_item.mac.path; - - // Check if this is one of our known style macros - let macro_name = path_to_string(path); - - // Handle the known macros by calling their implementations - match macro_name.as_str() { - "gpui_macros::style_helpers" | "style_helpers" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::style_helpers(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::visibility_style_methods" | "visibility_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::visibility_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::margin_style_methods" | "margin_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::margin_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::padding_style_methods" | "padding_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::padding_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::position_style_methods" | "position_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::position_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::overflow_style_methods" | "overflow_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::overflow_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::cursor_style_methods" | "cursor_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::cursor_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::border_style_methods" | "border_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::border_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - "gpui_macros::box_shadow_style_methods" | "box_shadow_style_methods" => { - let tokens = macro_item.mac.tokens.clone(); - let expanded = crate::styles::box_shadow_style_methods(TokenStream::from(tokens)); - parse_expanded_items(expanded) - } - _ => None, - } -} - -fn path_to_string(path: &Path) -> String { - path.segments - .iter() - .map(|seg| seg.ident.to_string()) - .collect::>() - .join("::") -} - -fn parse_expanded_items(expanded: TokenStream) -> Option> { - let tokens = TokenStream2::from(expanded); - - // Try to parse the expanded tokens as trait items - // We need to wrap them in a dummy trait to parse properly - let dummy_trait: ItemTrait = parse_quote! { - trait Dummy { - #tokens - } - }; - - Some(dummy_trait.items) -} diff --git a/crates/gpui_macros/src/derive_into_element.rs b/crates/gpui_macros/src/derive_into_element.rs deleted file mode 100644 index 89d609ae65..0000000000 --- a/crates/gpui_macros/src/derive_into_element.rs +++ /dev/null @@ -1,24 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, parse_macro_input}; - -pub fn derive_into_element(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - - let r#gen = quote! { - impl #impl_generics gpui::IntoElement for #type_name #type_generics - #where_clause - { - type Element = gpui::Component; - - #[track_caller] - fn into_element(self) -> Self::Element { - gpui::Component::new(self) - } - } - }; - - r#gen.into() -} diff --git a/crates/gpui_macros/src/derive_render.rs b/crates/gpui_macros/src/derive_render.rs deleted file mode 100644 index 3e0dcbc993..0000000000 --- a/crates/gpui_macros/src/derive_render.rs +++ /dev/null @@ -1,21 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, parse_macro_input}; - -pub fn derive_render(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - - let r#gen = quote! { - impl #impl_generics gpui::Render for #type_name #type_generics - #where_clause - { - fn render(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context) -> impl gpui::Element { - gpui::Empty - } - } - }; - - r#gen.into() -} diff --git a/crates/gpui_macros/src/derive_visual_context.rs b/crates/gpui_macros/src/derive_visual_context.rs deleted file mode 100644 index f2681bb29b..0000000000 --- a/crates/gpui_macros/src/derive_visual_context.rs +++ /dev/null @@ -1,71 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, parse_macro_input}; - -use super::get_simple_attribute_field; - -pub fn derive_visual_context(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - - let Some(window_variable) = get_simple_attribute_field(&ast, "window") else { - return quote! { - compile_error!("Derive must have a #[window] attribute to detect the &mut Window field"); - } - .into(); - }; - - let Some(app_variable) = get_simple_attribute_field(&ast, "app") else { - return quote! { - compile_error!("Derive must have a #[app] attribute to detect the &mut App field"); - } - .into(); - }; - - let type_name = &ast.ident; - let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); - - let r#gen = quote! { - impl #impl_generics gpui::VisualContext for #type_name #type_generics - #where_clause - { - fn window_handle(&self) -> gpui::AnyWindowHandle { - self.#window_variable.window_handle() - } - - fn update_window_entity( - &mut self, - entity: &gpui::Entity, - update: impl FnOnce(&mut T, &mut gpui::Window, &mut gpui::Context) -> R, - ) -> Self::Result { - gpui::AppContext::update_entity(self.#app_variable, entity, |entity, cx| update(entity, self.#window_variable, cx)) - } - - fn new_window_entity( - &mut self, - build_entity: impl FnOnce(&mut gpui::Window, &mut gpui::Context<'_, T>) -> T, - ) -> Self::Result> { - gpui::AppContext::new(self.#app_variable, |cx| build_entity(self.#window_variable, cx)) - } - - fn replace_root_view( - &mut self, - build_view: impl FnOnce(&mut gpui::Window, &mut gpui::Context) -> V, - ) -> Self::Result> - where - V: 'static + gpui::Render, - { - self.#window_variable.replace_root(self.#app_variable, build_view) - } - - fn focus(&mut self, entity: &gpui::Entity) -> Self::Result<()> - where - V: gpui::Focusable, - { - let focus_handle = gpui::Focusable::focus_handle(entity, self.#app_variable); - self.#window_variable.focus(&focus_handle) - } - } - }; - - r#gen.into() -} diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs deleted file mode 100644 index 0f1365be77..0000000000 --- a/crates/gpui_macros/src/gpui_macros.rs +++ /dev/null @@ -1,223 +0,0 @@ -mod derive_action; -mod derive_app_context; -mod derive_into_element; -mod derive_render; -mod derive_visual_context; -mod register_action; -mod styles; -mod test; - -#[cfg(any(feature = "inspector", debug_assertions))] -mod derive_inspector_reflection; - -use proc_macro::TokenStream; -use syn::{DeriveInput, Ident}; - -/// `Action` derive macro - see the trait documentation for details. -#[proc_macro_derive(Action, attributes(action))] -pub fn derive_action(input: TokenStream) -> TokenStream { - derive_action::derive_action(input) -} - -/// This can be used to register an action with the GPUI runtime when you want to manually implement -/// the `Action` trait. Typically you should use the `Action` derive macro or `actions!` macro -/// instead. -#[proc_macro] -pub fn register_action(ident: TokenStream) -> TokenStream { - register_action::register_action(ident) -} - -/// #[derive(IntoElement)] is used to create a Component out of anything that implements -/// the `RenderOnce` trait. -#[proc_macro_derive(IntoElement)] -pub fn derive_into_element(input: TokenStream) -> TokenStream { - derive_into_element::derive_into_element(input) -} - -#[proc_macro_derive(Render)] -#[doc(hidden)] -pub fn derive_render(input: TokenStream) -> TokenStream { - derive_render::derive_render(input) -} - -/// #[derive(AppContext)] is used to create a context out of anything that holds a `&mut App` -/// Note that a `#[app]` attribute is required to identify the variable holding the &mut App. -/// -/// Failure to add the attribute causes a compile error: -/// -/// ```compile_fail -/// # #[macro_use] extern crate gpui_macros; -/// # #[macro_use] extern crate gpui; -/// #[derive(AppContext)] -/// struct MyContext<'a> { -/// app: &'a mut gpui::App -/// } -/// ``` -#[proc_macro_derive(AppContext, attributes(app))] -pub fn derive_app_context(input: TokenStream) -> TokenStream { - derive_app_context::derive_app_context(input) -} - -/// #[derive(VisualContext)] is used to create a visual context out of anything that holds a `&mut Window` and -/// implements `AppContext` -/// Note that a `#[app]` and a `#[window]` attribute are required to identify the variables holding the &mut App, -/// and &mut Window respectively. -/// -/// Failure to add both attributes causes a compile error: -/// -/// ```compile_fail -/// # #[macro_use] extern crate gpui_macros; -/// # #[macro_use] extern crate gpui; -/// #[derive(VisualContext)] -/// struct MyContext<'a, 'b> { -/// #[app] -/// app: &'a mut gpui::App, -/// window: &'b mut gpui::Window -/// } -/// ``` -/// -/// ```compile_fail -/// # #[macro_use] extern crate gpui_macros; -/// # #[macro_use] extern crate gpui; -/// #[derive(VisualContext)] -/// struct MyContext<'a, 'b> { -/// app: &'a mut gpui::App, -/// #[window] -/// window: &'b mut gpui::Window -/// } -/// ``` -#[proc_macro_derive(VisualContext, attributes(window, app))] -pub fn derive_visual_context(input: TokenStream) -> TokenStream { - derive_visual_context::derive_visual_context(input) -} - -/// Used by GPUI to generate the style helpers. -#[proc_macro] -#[doc(hidden)] -pub fn style_helpers(input: TokenStream) -> TokenStream { - styles::style_helpers(input) -} - -/// Generates methods for visibility styles. -#[proc_macro] -pub fn visibility_style_methods(input: TokenStream) -> TokenStream { - styles::visibility_style_methods(input) -} - -/// Generates methods for margin styles. -#[proc_macro] -pub fn margin_style_methods(input: TokenStream) -> TokenStream { - styles::margin_style_methods(input) -} - -/// Generates methods for padding styles. -#[proc_macro] -pub fn padding_style_methods(input: TokenStream) -> TokenStream { - styles::padding_style_methods(input) -} - -/// Generates methods for position styles. -#[proc_macro] -pub fn position_style_methods(input: TokenStream) -> TokenStream { - styles::position_style_methods(input) -} - -/// Generates methods for overflow styles. -#[proc_macro] -pub fn overflow_style_methods(input: TokenStream) -> TokenStream { - styles::overflow_style_methods(input) -} - -/// Generates methods for cursor styles. -#[proc_macro] -pub fn cursor_style_methods(input: TokenStream) -> TokenStream { - styles::cursor_style_methods(input) -} - -/// Generates methods for border styles. -#[proc_macro] -pub fn border_style_methods(input: TokenStream) -> TokenStream { - styles::border_style_methods(input) -} - -/// Generates methods for box shadow styles. -#[proc_macro] -pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { - styles::box_shadow_style_methods(input) -} - -/// `#[gpui::test]` can be used to annotate test functions that run with GPUI support. -/// -/// It supports both synchronous and asynchronous tests, and can provide you with -/// as many `TestAppContext` instances as you need. -/// The output contains a `#[test]` annotation so this can be used with any existing -/// test harness (`cargo test` or `cargo-nextest`). -/// -/// ``` -/// #[gpui::test] -/// async fn test_foo(mut cx: &TestAppContext) { } -/// ``` -/// -/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance. -/// this will be seeded with the `SEED` environment variable and is used internally by -/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests. -/// Using the same `StdRng` for behavior in your test will allow you to exercise a wide -/// variety of scenarios and interleavings just by changing the seed. -/// -/// # Arguments -/// -/// - `#[gpui::test]` with no arguments runs once with the seed `0` or `SEED` env var if set. -/// - `#[gpui::test(seed = 10)]` runs once with the seed `10`. -/// - `#[gpui::test(seeds(10, 20, 30))]` runs three times with seeds `10`, `20`, and `30`. -/// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`. -/// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass. -/// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the -/// tests fail so that you can write out more detail about the failure. -/// -/// You can combine `iterations = ...` with `seeds(...)`: -/// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`. -/// - `#[gpui::test(iterations = 5, seeds(10, 20, 30)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10, 20, 30))]`. -/// - `#[gpui::test(seeds(10, 20, 30), iterations = 5]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10, 20, 30))]`. -/// -/// # Environment Variables -/// -/// - `SEED`: sets a seed for the first run -/// - `ITERATIONS`: forces the value of the `iterations` argument -#[proc_macro_attribute] -pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { - test::test(args, function) -} - -/// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides -/// enumeration and lookup by name of all methods that have the shape `fn method(self) -> Self`. -/// This is used by the inspector so that it can use the builder methods in `Styled` and -/// `StyledExt`. -/// -/// The generated module will have the name `_reflection` and contain the -/// following functions: -/// -/// ```ignore -/// pub fn methods::() -> Vec>; -/// -/// pub fn find_method::() -> Option>; -/// ``` -/// -/// The `invoke` method on `FunctionReflection` will run the method. `FunctionReflection` also -/// provides the method's documentation. -#[cfg(any(feature = "inspector", debug_assertions))] -#[proc_macro_attribute] -pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream { - derive_inspector_reflection::derive_inspector_reflection(_args, input) -} - -pub(crate) fn get_simple_attribute_field(ast: &DeriveInput, name: &'static str) -> Option { - match &ast.data { - syn::Data::Struct(data_struct) => data_struct - .fields - .iter() - .find(|field| field.attrs.iter().any(|attr| attr.path().is_ident(name))) - .map(|field| field.ident.clone().unwrap()), - syn::Data::Enum(_) => None, - syn::Data::Union(_) => None, - } -} diff --git a/crates/gpui_macros/src/register_action.rs b/crates/gpui_macros/src/register_action.rs deleted file mode 100644 index ca36ce3186..0000000000 --- a/crates/gpui_macros/src/register_action.rs +++ /dev/null @@ -1,47 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::{Ident, TokenStream as TokenStream2}; -use quote::{format_ident, quote}; -use syn::parse_macro_input; - -pub(crate) fn register_action(ident: TokenStream) -> TokenStream { - let name = parse_macro_input!(ident as Ident); - let registration = generate_register_action(&name); - - TokenStream::from(quote! { - #registration - }) -} - -pub(crate) fn generate_register_action(type_name: &Ident) -> TokenStream2 { - let action_builder_fn_name = format_ident!( - "__gpui_actions_builder_{}", - type_name.to_string().to_lowercase() - ); - - quote! { - impl #type_name { - /// This is an auto generated function, do not use. - #[automatically_derived] - #[doc(hidden)] - fn __autogenerated() { - /// This is an auto generated function, do not use. - #[doc(hidden)] - fn #action_builder_fn_name() -> gpui::MacroActionData { - gpui::MacroActionData { - name: <#type_name as gpui::Action>::name_for_type(), - type_id: ::std::any::TypeId::of::<#type_name>(), - build: <#type_name as gpui::Action>::build, - json_schema: <#type_name as gpui::Action>::action_json_schema, - deprecated_aliases: <#type_name as gpui::Action>::deprecated_aliases(), - deprecation_message: <#type_name as gpui::Action>::deprecation_message(), - documentation: <#type_name as gpui::Action>::documentation(), - } - } - - gpui::private::inventory::submit! { - gpui::MacroActionBuilder(#action_builder_fn_name) - } - } - } - } -} diff --git a/crates/gpui_macros/src/styles.rs b/crates/gpui_macros/src/styles.rs deleted file mode 100644 index 36d46cfb51..0000000000 --- a/crates/gpui_macros/src/styles.rs +++ /dev/null @@ -1,1462 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::{ - Token, Visibility, braced, - parse::{Parse, ParseStream, Result}, - parse_macro_input, -}; - -#[derive(Debug)] -struct StyleableMacroInput { - method_visibility: Visibility, -} - -impl Parse for StyleableMacroInput { - fn parse(input: ParseStream) -> Result { - if !input.peek(syn::token::Brace) { - return Ok(Self { - method_visibility: Visibility::Inherited, - }); - } - - let content; - braced!(content in input); - - let mut method_visibility = None; - - let ident: syn::Ident = content.parse()?; - if ident == "visibility" { - let _colon: Token![:] = content.parse()?; - method_visibility = Some(content.parse()?); - } - - Ok(Self { - method_visibility: method_visibility.unwrap_or(Visibility::Inherited), - }) - } -} - -pub fn style_helpers(input: TokenStream) -> TokenStream { - let _ = parse_macro_input!(input as StyleableMacroInput); - let methods = generate_methods(); - let output = quote! { - #(#methods)* - }; - - output.into() -} - -pub fn visibility_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Sets the visibility of the element to `visible`. - /// [Docs](https://tailwindcss.com/docs/visibility) - #visibility fn visible(mut self) -> Self { - self.style().visibility = Some(gpui::Visibility::Visible); - self - } - - /// Sets the visibility of the element to `hidden`. - /// [Docs](https://tailwindcss.com/docs/visibility) - #visibility fn invisible(mut self) -> Self { - self.style().visibility = Some(gpui::Visibility::Hidden); - self - } - }; - - output.into() -} - -pub fn margin_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let methods = generate_box_style_methods( - margin_box_style_prefixes(), - box_style_suffixes(), - input.method_visibility, - ); - let output = quote! { - #(#methods)* - }; - - output.into() -} - -pub fn padding_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let methods = generate_box_style_methods( - padding_box_style_prefixes(), - box_style_suffixes(), - input.method_visibility, - ); - let output = quote! { - #(#methods)* - }; - - output.into() -} - -pub fn position_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let methods = generate_box_style_methods( - position_box_style_prefixes(), - box_style_suffixes(), - visibility.clone(), - ); - let output = quote! { - /// Sets the position of the element to `relative`. - /// [Docs](https://tailwindcss.com/docs/position) - #visibility fn relative(mut self) -> Self { - self.style().position = Some(gpui::Position::Relative); - self - } - - /// Sets the position of the element to `absolute`. - /// [Docs](https://tailwindcss.com/docs/position) - #visibility fn absolute(mut self) -> Self { - self.style().position = Some(gpui::Position::Absolute); - self - } - - #(#methods)* - }; - - output.into() -} - -pub fn overflow_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Sets the behavior of content that overflows the container to be hidden. - /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) - #visibility fn overflow_hidden(mut self) -> Self { - self.style().overflow.x = Some(gpui::Overflow::Hidden); - self.style().overflow.y = Some(gpui::Overflow::Hidden); - self - } - - /// Sets the behavior of content that overflows the container on the X axis to be hidden. - /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) - #visibility fn overflow_x_hidden(mut self) -> Self { - self.style().overflow.x = Some(gpui::Overflow::Hidden); - self - } - - /// Sets the behavior of content that overflows the container on the Y axis to be hidden. - /// [Docs](https://tailwindcss.com/docs/overflow#hiding-content-that-overflows) - #visibility fn overflow_y_hidden(mut self) -> Self { - self.style().overflow.y = Some(gpui::Overflow::Hidden); - self - } - }; - - output.into() -} - -pub fn cursor_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Set the cursor style when hovering over this element - #visibility fn cursor(mut self, cursor: CursorStyle) -> Self { - self.style().mouse_cursor = Some(cursor); - self - } - - /// Sets the cursor style when hovering an element to `default`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_default(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::Arrow); - self - } - - /// Sets the cursor style when hovering an element to `pointer`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_pointer(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::PointingHand); - self - } - - /// Sets cursor style when hovering over an element to `text`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_text(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::IBeam); - self - } - - /// Sets cursor style when hovering over an element to `move`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_move(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ClosedHand); - self - } - - /// Sets cursor style when hovering over an element to `not-allowed`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_not_allowed(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::OperationNotAllowed); - self - } - - /// Sets cursor style when hovering over an element to `context-menu`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_context_menu(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ContextualMenu); - self - } - - /// Sets cursor style when hovering over an element to `crosshair`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_crosshair(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::Crosshair); - self - } - - /// Sets cursor style when hovering over an element to `vertical-text`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_vertical_text(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::IBeamCursorForVerticalLayout); - self - } - - /// Sets cursor style when hovering over an element to `alias`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_alias(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::DragLink); - self - } - - /// Sets cursor style when hovering over an element to `copy`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_copy(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::DragCopy); - self - } - - /// Sets cursor style when hovering over an element to `no-drop`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_no_drop(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::OperationNotAllowed); - self - } - - /// Sets cursor style when hovering over an element to `grab`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_grab(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::OpenHand); - self - } - - /// Sets cursor style when hovering over an element to `grabbing`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_grabbing(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ClosedHand); - self - } - - /// Sets cursor style when hovering over an element to `ew-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_ew_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeftRight); - self - } - - /// Sets cursor style when hovering over an element to `ns-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_ns_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpDown); - self - } - - /// Sets cursor style when hovering over an element to `nesw-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_nesw_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpRightDownLeft); - self - } - - /// Sets cursor style when hovering over an element to `nwse-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_nwse_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUpLeftDownRight); - self - } - - /// Sets cursor style when hovering over an element to `col-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_col_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeColumn); - self - } - - /// Sets cursor style when hovering over an element to `row-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_row_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeRow); - self - } - - /// Sets cursor style when hovering over an element to `n-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_n_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeUp); - self - } - - /// Sets cursor style when hovering over an element to `e-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_e_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeRight); - self - } - - /// Sets cursor style when hovering over an element to `s-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_s_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeDown); - self - } - - /// Sets cursor style when hovering over an element to `w-resize`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_w_resize(mut self) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::ResizeLeft); - self - } - - /// Sets cursor style when hovering over an element to `none`. - /// [Docs](https://tailwindcss.com/docs/cursor) - #visibility fn cursor_none(mut self, cursor: CursorStyle) -> Self { - self.style().mouse_cursor = Some(gpui::CursorStyle::None); - self - } - }; - - output.into() -} - -pub fn border_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - - let mut methods = Vec::new(); - - for border_style_prefix in border_prefixes() { - methods.push(generate_custom_value_setter( - visibility.clone(), - border_style_prefix.prefix, - quote! { AbsoluteLength }, - &border_style_prefix.fields, - border_style_prefix.doc_string_prefix, - )); - - for border_style_suffix in border_suffixes() { - methods.push(generate_predefined_setter( - visibility.clone(), - border_style_prefix.prefix, - border_style_suffix.suffix, - &border_style_prefix.fields, - &border_style_suffix.width_tokens, - false, - &format!( - "{prefix}\n\n{suffix}", - prefix = border_style_prefix.doc_string_prefix, - suffix = border_style_suffix.doc_string_suffix, - ), - )); - } - } - - let output = quote! { - /// Sets the border color of the element. - #visibility fn border_color(mut self, border_color: C) -> Self - where - C: Into, - Self: Sized, - { - self.style().border_color = Some(border_color.into()); - self - } - - #(#methods)* - }; - - output.into() -} - -pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as StyleableMacroInput); - let visibility = input.method_visibility; - let output = quote! { - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow(mut self, shadows: std::vec::Vec) -> Self { - self.style().box_shadow = Some(shadows); - self - } - - /// Clears the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_none(mut self) -> Self { - self.style().box_shadow = Some(Default::default()); - self - } - - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_2xs(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; - - self.style().box_shadow = Some(vec![BoxShadow { - color: hsla(0., 0., 0., 0.05), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]); - self - } - - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_xs(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; - - self.style().box_shadow = Some(vec![BoxShadow { - color: hsla(0., 0., 0., 0.05), - offset: point(px(0.), px(1.)), - blur_radius: px(2.), - spread_radius: px(0.), - }]); - self - } - - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_sm(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; - - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(1.)), - blur_radius: px(3.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(1.)), - blur_radius: px(2.), - spread_radius: px(-1.), - } - ]); - self - } - - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_md(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; - - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(4.)), - blur_radius: px(6.), - spread_radius: px(-1.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(2.)), - blur_radius: px(4.), - spread_radius: px(-2.), - } - ]); - self - } - - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_lg(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; - - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(10.)), - blur_radius: px(15.), - spread_radius: px(-3.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(4.)), - blur_radius: px(6.), - spread_radius: px(-4.), - } - ]); - self - } - - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_xl(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; - - self.style().box_shadow = Some(vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(20.)), - blur_radius: px(25.), - spread_radius: px(-5.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.1), - offset: point(px(0.), px(8.)), - blur_radius: px(10.), - spread_radius: px(-6.), - } - ]); - self - } - - /// Sets the box shadow of the element. - /// [Docs](https://tailwindcss.com/docs/box-shadow) - #visibility fn shadow_2xl(mut self) -> Self { - use gpui::{BoxShadow, hsla, point, px}; - use std::vec; - - self.style().box_shadow = Some(vec![BoxShadow { - color: hsla(0., 0., 0., 0.25), - offset: point(px(0.), px(25.)), - blur_radius: px(50.), - spread_radius: px(-12.), - }]); - self - } - }; - - output.into() -} - -struct BoxStylePrefix { - prefix: &'static str, - auto_allowed: bool, - fields: Vec, - doc_string_prefix: &'static str, -} - -struct BoxStyleSuffix { - suffix: &'static str, - length_tokens: TokenStream2, - doc_string_suffix: &'static str, -} - -struct CornerStylePrefix { - prefix: &'static str, - fields: Vec, - doc_string_prefix: &'static str, -} - -struct CornerStyleSuffix { - suffix: &'static str, - radius_tokens: TokenStream2, - doc_string_suffix: &'static str, -} - -struct BorderStylePrefix { - prefix: &'static str, - fields: Vec, - doc_string_prefix: &'static str, -} - -struct BorderStyleSuffix { - suffix: &'static str, - width_tokens: TokenStream2, - doc_string_suffix: &'static str, -} - -fn generate_box_style_methods( - prefixes: Vec, - suffixes: Vec, - visibility: Visibility, -) -> Vec { - let mut methods = Vec::new(); - - for box_style_prefix in prefixes { - methods.push(generate_custom_value_setter( - visibility.clone(), - box_style_prefix.prefix, - if box_style_prefix.auto_allowed { - quote! { Length } - } else { - quote! { DefiniteLength } - }, - &box_style_prefix.fields, - box_style_prefix.doc_string_prefix, - )); - - for box_style_suffix in &suffixes { - if box_style_suffix.suffix != "auto" || box_style_prefix.auto_allowed { - methods.push(generate_predefined_setter( - visibility.clone(), - box_style_prefix.prefix, - box_style_suffix.suffix, - &box_style_prefix.fields, - &box_style_suffix.length_tokens, - false, - &format!( - "{prefix}\n\n{suffix}", - prefix = box_style_prefix.doc_string_prefix, - suffix = box_style_suffix.doc_string_suffix, - ), - )); - } - - if box_style_suffix.suffix != "auto" { - methods.push(generate_predefined_setter( - visibility.clone(), - box_style_prefix.prefix, - box_style_suffix.suffix, - &box_style_prefix.fields, - &box_style_suffix.length_tokens, - true, - &format!( - "{prefix}\n\n{suffix}", - prefix = box_style_prefix.doc_string_prefix, - suffix = box_style_suffix.doc_string_suffix, - ), - )); - } - } - } - - methods -} - -fn generate_methods() -> Vec { - let visibility = Visibility::Inherited; - let mut methods = - generate_box_style_methods(box_prefixes(), box_style_suffixes(), visibility.clone()); - - for corner_style_prefix in corner_prefixes() { - methods.push(generate_custom_value_setter( - visibility.clone(), - corner_style_prefix.prefix, - quote! { AbsoluteLength }, - &corner_style_prefix.fields, - corner_style_prefix.doc_string_prefix, - )); - - for corner_style_suffix in corner_suffixes() { - methods.push(generate_predefined_setter( - visibility.clone(), - corner_style_prefix.prefix, - corner_style_suffix.suffix, - &corner_style_prefix.fields, - &corner_style_suffix.radius_tokens, - false, - &format!( - "{prefix}\n\n{suffix}", - prefix = corner_style_prefix.doc_string_prefix, - suffix = corner_style_suffix.doc_string_suffix, - ), - )); - } - } - - methods -} - -fn generate_predefined_setter( - visibility: Visibility, - name: &'static str, - length: &'static str, - fields: &[TokenStream2], - length_tokens: &TokenStream2, - negate: bool, - doc_string: &str, -) -> TokenStream2 { - let (negation_qualifier, negation_token) = if negate { - ("_neg", quote! { - }) - } else { - ("", quote! {}) - }; - - let method_name = if length.is_empty() { - format_ident!("{name}{negation_qualifier}") - } else { - format_ident!("{name}{negation_qualifier}_{length}") - }; - - let field_assignments = fields - .iter() - .map(|field_tokens| { - quote! { - style.#field_tokens = Some((#negation_token gpui::#length_tokens).into()); - } - }) - .collect::>(); - - let method = quote! { - #[doc = #doc_string] - #visibility fn #method_name(mut self) -> Self { - let style = self.style(); - #(#field_assignments)* - self - } - }; - - method -} - -fn generate_custom_value_setter( - visibility: Visibility, - prefix: &str, - length_type: TokenStream2, - fields: &[TokenStream2], - doc_string: &str, -) -> TokenStream2 { - let method_name = format_ident!("{}", prefix); - - let mut iter = fields.iter(); - let last = iter.next_back().unwrap(); - let field_assignments = iter - .map(|field_tokens| { - quote! { - style.#field_tokens = Some(length.clone().into()); - } - }) - .chain(std::iter::once(quote! { - style.#last = Some(length.into()); - })) - .collect::>(); - - let method = quote! { - #[doc = #doc_string] - #visibility fn #method_name(mut self, length: impl std::clone::Clone + Into) -> Self { - let style = self.style(); - #(#field_assignments)* - self - } - }; - - method -} - -fn margin_box_style_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "m", - auto_allowed: true, - fields: vec![ - quote! { margin.top }, - quote! { margin.bottom }, - quote! { margin.left }, - quote! { margin.right }, - ], - doc_string_prefix: "Sets the margin of the element. [Docs](https://tailwindcss.com/docs/margin)", - }, - BoxStylePrefix { - prefix: "mt", - auto_allowed: true, - fields: vec![quote! { margin.top }], - doc_string_prefix: "Sets the top margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "mb", - auto_allowed: true, - fields: vec![quote! { margin.bottom }], - doc_string_prefix: "Sets the bottom margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "my", - auto_allowed: true, - fields: vec![quote! { margin.top }, quote! { margin.bottom }], - doc_string_prefix: "Sets the vertical margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-vertical-margin)", - }, - BoxStylePrefix { - prefix: "mx", - auto_allowed: true, - fields: vec![quote! { margin.left }, quote! { margin.right }], - doc_string_prefix: "Sets the horizontal margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-horizontal-margin)", - }, - BoxStylePrefix { - prefix: "ml", - auto_allowed: true, - fields: vec![quote! { margin.left }], - doc_string_prefix: "Sets the left margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "mr", - auto_allowed: true, - fields: vec![quote! { margin.right }], - doc_string_prefix: "Sets the right margin of the element. [Docs](https://tailwindcss.com/docs/margin#add-margin-to-a-single-side)", - }, - ] -} - -fn padding_box_style_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "p", - auto_allowed: false, - fields: vec![ - quote! { padding.top }, - quote! { padding.bottom }, - quote! { padding.left }, - quote! { padding.right }, - ], - doc_string_prefix: "Sets the padding of the element. [Docs](https://tailwindcss.com/docs/padding)", - }, - BoxStylePrefix { - prefix: "pt", - auto_allowed: false, - fields: vec![quote! { padding.top }], - doc_string_prefix: "Sets the top padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "pb", - auto_allowed: false, - fields: vec![quote! { padding.bottom }], - doc_string_prefix: "Sets the bottom padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "px", - auto_allowed: false, - fields: vec![quote! { padding.left }, quote! { padding.right }], - doc_string_prefix: "Sets the horizontal padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-horizontal-padding)", - }, - BoxStylePrefix { - prefix: "py", - auto_allowed: false, - fields: vec![quote! { padding.top }, quote! { padding.bottom }], - doc_string_prefix: "Sets the vertical padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-vertical-padding)", - }, - BoxStylePrefix { - prefix: "pl", - auto_allowed: false, - fields: vec![quote! { padding.left }], - doc_string_prefix: "Sets the left padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - BoxStylePrefix { - prefix: "pr", - auto_allowed: false, - fields: vec![quote! { padding.right }], - doc_string_prefix: "Sets the right padding of the element. [Docs](https://tailwindcss.com/docs/padding#add-padding-to-a-single-side)", - }, - ] -} - -fn position_box_style_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "inset", - auto_allowed: true, - fields: vec![ - quote! { inset.top }, - quote! { inset.right }, - quote! { inset.bottom }, - quote! { inset.left }, - ], - doc_string_prefix: "Sets the top, right, bottom, and left values of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "top", - auto_allowed: true, - fields: vec![quote! { inset.top }], - doc_string_prefix: "Sets the top value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "bottom", - auto_allowed: true, - fields: vec![quote! { inset.bottom }], - doc_string_prefix: "Sets the bottom value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "left", - auto_allowed: true, - fields: vec![quote! { inset.left }], - doc_string_prefix: "Sets the left value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - BoxStylePrefix { - prefix: "right", - auto_allowed: true, - fields: vec![quote! { inset.right }], - doc_string_prefix: "Sets the right value of a positioned element. [Docs](https://tailwindcss.com/docs/top-right-bottom-left)", - }, - ] -} - -fn box_prefixes() -> Vec { - vec![ - BoxStylePrefix { - prefix: "w", - auto_allowed: true, - fields: vec![quote! { size.width }], - doc_string_prefix: "Sets the width of the element. [Docs](https://tailwindcss.com/docs/width)", - }, - BoxStylePrefix { - prefix: "h", - auto_allowed: true, - fields: vec![quote! { size.height }], - doc_string_prefix: "Sets the height of the element. [Docs](https://tailwindcss.com/docs/height)", - }, - BoxStylePrefix { - prefix: "size", - auto_allowed: true, - fields: vec![quote! {size.width}, quote! {size.height}], - doc_string_prefix: "Sets the width and height of the element.", - }, - // TODO: These don't use the same size ramp as the others - // see https://tailwindcss.com/docs/max-width - BoxStylePrefix { - prefix: "min_w", - auto_allowed: true, - fields: vec![quote! { min_size.width }], - doc_string_prefix: "Sets the minimum width of the element. [Docs](https://tailwindcss.com/docs/min-width)", - }, - // TODO: These don't use the same size ramp as the others - // see https://tailwindcss.com/docs/max-width - BoxStylePrefix { - prefix: "min_h", - auto_allowed: true, - fields: vec![quote! { min_size.height }], - doc_string_prefix: "Sets the minimum height of the element. [Docs](https://tailwindcss.com/docs/min-height)", - }, - // TODO: These don't use the same size ramp as the others - // see https://tailwindcss.com/docs/max-width - BoxStylePrefix { - prefix: "max_w", - auto_allowed: true, - fields: vec![quote! { max_size.width }], - doc_string_prefix: "Sets the maximum width of the element. [Docs](https://tailwindcss.com/docs/max-width)", - }, - // TODO: These don't use the same size ramp as the others - // see https://tailwindcss.com/docs/max-width - BoxStylePrefix { - prefix: "max_h", - auto_allowed: true, - fields: vec![quote! { max_size.height }], - doc_string_prefix: "Sets the maximum height of the element. [Docs](https://tailwindcss.com/docs/max-height)", - }, - BoxStylePrefix { - prefix: "gap", - auto_allowed: false, - fields: vec![quote! { gap.width }, quote! { gap.height }], - doc_string_prefix: "Sets the gap between rows and columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap)", - }, - BoxStylePrefix { - prefix: "gap_x", - auto_allowed: false, - fields: vec![quote! { gap.width }], - doc_string_prefix: "Sets the gap between columns in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)", - }, - BoxStylePrefix { - prefix: "gap_y", - auto_allowed: false, - fields: vec![quote! { gap.height }], - doc_string_prefix: "Sets the gap between rows in flex layouts. [Docs](https://tailwindcss.com/docs/gap#changing-row-and-column-gaps-independently)", - }, - ] -} - -fn box_style_suffixes() -> Vec { - vec![ - BoxStyleSuffix { - suffix: "0", - length_tokens: quote! { px(0.) }, - doc_string_suffix: "0px", - }, - BoxStyleSuffix { - suffix: "0p5", - length_tokens: quote! { rems(0.125) }, - doc_string_suffix: "2px (0.125rem)", - }, - BoxStyleSuffix { - suffix: "1", - length_tokens: quote! { rems(0.25) }, - doc_string_suffix: "4px (0.25rem)", - }, - BoxStyleSuffix { - suffix: "1p5", - length_tokens: quote! { rems(0.375) }, - doc_string_suffix: "6px (0.375rem)", - }, - BoxStyleSuffix { - suffix: "2", - length_tokens: quote! { rems(0.5) }, - doc_string_suffix: "8px (0.5rem)", - }, - BoxStyleSuffix { - suffix: "2p5", - length_tokens: quote! { rems(0.625) }, - doc_string_suffix: "10px (0.625rem)", - }, - BoxStyleSuffix { - suffix: "3", - length_tokens: quote! { rems(0.75) }, - doc_string_suffix: "12px (0.75rem)", - }, - BoxStyleSuffix { - suffix: "3p5", - length_tokens: quote! { rems(0.875) }, - doc_string_suffix: "14px (0.875rem)", - }, - BoxStyleSuffix { - suffix: "4", - length_tokens: quote! { rems(1.) }, - doc_string_suffix: "16px (1rem)", - }, - BoxStyleSuffix { - suffix: "5", - length_tokens: quote! { rems(1.25) }, - doc_string_suffix: "20px (1.25rem)", - }, - BoxStyleSuffix { - suffix: "6", - length_tokens: quote! { rems(1.5) }, - doc_string_suffix: "24px (1.5rem)", - }, - BoxStyleSuffix { - suffix: "7", - length_tokens: quote! { rems(1.75) }, - doc_string_suffix: "28px (1.75rem)", - }, - BoxStyleSuffix { - suffix: "8", - length_tokens: quote! { rems(2.0) }, - doc_string_suffix: "32px (2rem)", - }, - BoxStyleSuffix { - suffix: "9", - length_tokens: quote! { rems(2.25) }, - doc_string_suffix: "36px (2.25rem)", - }, - BoxStyleSuffix { - suffix: "10", - length_tokens: quote! { rems(2.5) }, - doc_string_suffix: "40px (2.5rem)", - }, - BoxStyleSuffix { - suffix: "11", - length_tokens: quote! { rems(2.75) }, - doc_string_suffix: "44px (2.75rem)", - }, - BoxStyleSuffix { - suffix: "12", - length_tokens: quote! { rems(3.) }, - doc_string_suffix: "48px (3rem)", - }, - BoxStyleSuffix { - suffix: "16", - length_tokens: quote! { rems(4.) }, - doc_string_suffix: "64px (4rem)", - }, - BoxStyleSuffix { - suffix: "20", - length_tokens: quote! { rems(5.) }, - doc_string_suffix: "80px (5rem)", - }, - BoxStyleSuffix { - suffix: "24", - length_tokens: quote! { rems(6.) }, - doc_string_suffix: "96px (6rem)", - }, - BoxStyleSuffix { - suffix: "32", - length_tokens: quote! { rems(8.) }, - doc_string_suffix: "128px (8rem)", - }, - BoxStyleSuffix { - suffix: "40", - length_tokens: quote! { rems(10.) }, - doc_string_suffix: "160px (10rem)", - }, - BoxStyleSuffix { - suffix: "48", - length_tokens: quote! { rems(12.) }, - doc_string_suffix: "192px (12rem)", - }, - BoxStyleSuffix { - suffix: "56", - length_tokens: quote! { rems(14.) }, - doc_string_suffix: "224px (14rem)", - }, - BoxStyleSuffix { - suffix: "64", - length_tokens: quote! { rems(16.) }, - doc_string_suffix: "256px (16rem)", - }, - BoxStyleSuffix { - suffix: "72", - length_tokens: quote! { rems(18.) }, - doc_string_suffix: "288px (18rem)", - }, - BoxStyleSuffix { - suffix: "80", - length_tokens: quote! { rems(20.) }, - doc_string_suffix: "320px (20rem)", - }, - BoxStyleSuffix { - suffix: "96", - length_tokens: quote! { rems(24.) }, - doc_string_suffix: "384px (24rem)", - }, - BoxStyleSuffix { - suffix: "112", - length_tokens: quote! { rems(28.) }, - doc_string_suffix: "448px (28rem)", - }, - BoxStyleSuffix { - suffix: "128", - length_tokens: quote! { rems(32.) }, - doc_string_suffix: "512px (32rem)", - }, - BoxStyleSuffix { - suffix: "auto", - length_tokens: quote! { auto() }, - doc_string_suffix: "Auto", - }, - BoxStyleSuffix { - suffix: "px", - length_tokens: quote! { px(1.) }, - doc_string_suffix: "1px", - }, - BoxStyleSuffix { - suffix: "full", - length_tokens: quote! { relative(1.) }, - doc_string_suffix: "100%", - }, - BoxStyleSuffix { - suffix: "1_2", - length_tokens: quote! { relative(0.5) }, - doc_string_suffix: "50% (1/2)", - }, - BoxStyleSuffix { - suffix: "1_3", - length_tokens: quote! { relative(1./3.) }, - doc_string_suffix: "33% (1/3)", - }, - BoxStyleSuffix { - suffix: "2_3", - length_tokens: quote! { relative(2./3.) }, - doc_string_suffix: "66% (2/3)", - }, - BoxStyleSuffix { - suffix: "1_4", - length_tokens: quote! { relative(0.25) }, - doc_string_suffix: "25% (1/4)", - }, - BoxStyleSuffix { - suffix: "2_4", - length_tokens: quote! { relative(0.5) }, - doc_string_suffix: "50% (2/4)", - }, - BoxStyleSuffix { - suffix: "3_4", - length_tokens: quote! { relative(0.75) }, - doc_string_suffix: "75% (3/4)", - }, - BoxStyleSuffix { - suffix: "1_5", - length_tokens: quote! { relative(0.2) }, - doc_string_suffix: "20% (1/5)", - }, - BoxStyleSuffix { - suffix: "2_5", - length_tokens: quote! { relative(0.4) }, - doc_string_suffix: "40% (2/5)", - }, - BoxStyleSuffix { - suffix: "3_5", - length_tokens: quote! { relative(0.6) }, - doc_string_suffix: "60% (3/5)", - }, - BoxStyleSuffix { - suffix: "4_5", - length_tokens: quote! { relative(0.8) }, - doc_string_suffix: "80% (4/5)", - }, - BoxStyleSuffix { - suffix: "1_6", - length_tokens: quote! { relative(1./6.) }, - doc_string_suffix: "16% (1/6)", - }, - BoxStyleSuffix { - suffix: "5_6", - length_tokens: quote! { relative(5./6.) }, - doc_string_suffix: "80% (5/6)", - }, - BoxStyleSuffix { - suffix: "1_12", - length_tokens: quote! { relative(1./12.) }, - doc_string_suffix: "8% (1/12)", - }, - ] -} - -fn corner_prefixes() -> Vec { - vec![ - CornerStylePrefix { - prefix: "rounded", - fields: vec![ - quote! { corner_radii.top_left }, - quote! { corner_radii.top_right }, - quote! { corner_radii.bottom_right }, - quote! { corner_radii.bottom_left }, - ], - doc_string_prefix: "Sets the border radius of the element. [Docs](https://tailwindcss.com/docs/border-radius)", - }, - CornerStylePrefix { - prefix: "rounded_t", - fields: vec![ - quote! { corner_radii.top_left }, - quote! { corner_radii.top_right }, - ], - doc_string_prefix: "Sets the border radius of the top side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_b", - fields: vec![ - quote! { corner_radii.bottom_left }, - quote! { corner_radii.bottom_right }, - ], - doc_string_prefix: "Sets the border radius of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_r", - fields: vec![ - quote! { corner_radii.top_right }, - quote! { corner_radii.bottom_right }, - ], - doc_string_prefix: "Sets the border radius of the right side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_l", - fields: vec![ - quote! { corner_radii.top_left }, - quote! { corner_radii.bottom_left }, - ], - doc_string_prefix: "Sets the border radius of the left side of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-sides-separately)", - }, - CornerStylePrefix { - prefix: "rounded_tl", - fields: vec![quote! { corner_radii.top_left }], - doc_string_prefix: "Sets the border radius of the top left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - CornerStylePrefix { - prefix: "rounded_tr", - fields: vec![quote! { corner_radii.top_right }], - doc_string_prefix: "Sets the border radius of the top right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - CornerStylePrefix { - prefix: "rounded_bl", - fields: vec![quote! { corner_radii.bottom_left }], - doc_string_prefix: "Sets the border radius of the bottom left corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - CornerStylePrefix { - prefix: "rounded_br", - fields: vec![quote! { corner_radii.bottom_right }], - doc_string_prefix: "Sets the border radius of the bottom right corner of the element. [Docs](https://tailwindcss.com/docs/border-radius#rounding-corners-separately)", - }, - ] -} - -fn corner_suffixes() -> Vec { - vec![ - CornerStyleSuffix { - suffix: "none", - radius_tokens: quote! { px(0.) }, - doc_string_suffix: "0px", - }, - CornerStyleSuffix { - suffix: "xs", - radius_tokens: quote! { rems(0.125) }, - doc_string_suffix: "2px (0.125rem)", - }, - CornerStyleSuffix { - suffix: "sm", - radius_tokens: quote! { rems(0.25) }, - doc_string_suffix: "4px (0.25rem)", - }, - CornerStyleSuffix { - suffix: "md", - radius_tokens: quote! { rems(0.375) }, - doc_string_suffix: "6px (0.375rem)", - }, - CornerStyleSuffix { - suffix: "lg", - radius_tokens: quote! { rems(0.5) }, - doc_string_suffix: "8px (0.5rem)", - }, - CornerStyleSuffix { - suffix: "xl", - radius_tokens: quote! { rems(0.75) }, - doc_string_suffix: "12px (0.75rem)", - }, - CornerStyleSuffix { - suffix: "2xl", - radius_tokens: quote! { rems(1.) }, - doc_string_suffix: "16px (1rem)", - }, - CornerStyleSuffix { - suffix: "3xl", - radius_tokens: quote! { rems(1.5) }, - doc_string_suffix: "24px (1.5rem)", - }, - CornerStyleSuffix { - suffix: "full", - radius_tokens: quote! { px(9999.) }, - doc_string_suffix: "9999px", - }, - ] -} - -fn border_prefixes() -> Vec { - vec![ - BorderStylePrefix { - prefix: "border", - fields: vec![ - quote! { border_widths.top }, - quote! { border_widths.right }, - quote! { border_widths.bottom }, - quote! { border_widths.left }, - ], - doc_string_prefix: "Sets the border width of the element. [Docs](https://tailwindcss.com/docs/border-width)", - }, - BorderStylePrefix { - prefix: "border_t", - fields: vec![quote! { border_widths.top }], - doc_string_prefix: "Sets the border width of the top side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_b", - fields: vec![quote! { border_widths.bottom }], - doc_string_prefix: "Sets the border width of the bottom side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_r", - fields: vec![quote! { border_widths.right }], - doc_string_prefix: "Sets the border width of the right side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_l", - fields: vec![quote! { border_widths.left }], - doc_string_prefix: "Sets the border width of the left side of the element. [Docs](https://tailwindcss.com/docs/border-width#individual-sides)", - }, - BorderStylePrefix { - prefix: "border_x", - fields: vec![ - quote! { border_widths.left }, - quote! { border_widths.right }, - ], - doc_string_prefix: "Sets the border width of the vertical sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)", - }, - BorderStylePrefix { - prefix: "border_y", - fields: vec![ - quote! { border_widths.top }, - quote! { border_widths.bottom }, - ], - doc_string_prefix: "Sets the border width of the horizontal sides of the element. [Docs](https://tailwindcss.com/docs/border-width#horizontal-and-vertical-sides)", - }, - ] -} - -fn border_suffixes() -> Vec { - vec![ - BorderStyleSuffix { - suffix: "0", - width_tokens: quote! { px(0.)}, - doc_string_suffix: "0px", - }, - BorderStyleSuffix { - suffix: "1", - width_tokens: quote! { px(1.) }, - doc_string_suffix: "1px", - }, - BorderStyleSuffix { - suffix: "2", - width_tokens: quote! { px(2.) }, - doc_string_suffix: "2px", - }, - BorderStyleSuffix { - suffix: "3", - width_tokens: quote! { px(3.) }, - doc_string_suffix: "3px", - }, - BorderStyleSuffix { - suffix: "4", - width_tokens: quote! { px(4.) }, - doc_string_suffix: "4px", - }, - BorderStyleSuffix { - suffix: "5", - width_tokens: quote! { px(5.) }, - doc_string_suffix: "5px", - }, - BorderStyleSuffix { - suffix: "6", - width_tokens: quote! { px(6.) }, - doc_string_suffix: "6px", - }, - BorderStyleSuffix { - suffix: "7", - width_tokens: quote! { px(7.) }, - doc_string_suffix: "7px", - }, - BorderStyleSuffix { - suffix: "8", - width_tokens: quote! { px(8.) }, - doc_string_suffix: "8px", - }, - BorderStyleSuffix { - suffix: "9", - width_tokens: quote! { px(9.) }, - doc_string_suffix: "9px", - }, - BorderStyleSuffix { - suffix: "10", - width_tokens: quote! { px(10.) }, - doc_string_suffix: "10px", - }, - BorderStyleSuffix { - suffix: "11", - width_tokens: quote! { px(11.) }, - doc_string_suffix: "11px", - }, - BorderStyleSuffix { - suffix: "12", - width_tokens: quote! { px(12.) }, - doc_string_suffix: "12px", - }, - BorderStyleSuffix { - suffix: "16", - width_tokens: quote! { px(16.) }, - doc_string_suffix: "16px", - }, - BorderStyleSuffix { - suffix: "20", - width_tokens: quote! { px(20.) }, - doc_string_suffix: "20px", - }, - BorderStyleSuffix { - suffix: "24", - width_tokens: quote! { px(24.) }, - doc_string_suffix: "24px", - }, - BorderStyleSuffix { - suffix: "32", - width_tokens: quote! { px(32.) }, - doc_string_suffix: "32px", - }, - ] -} diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs deleted file mode 100644 index 42ce304b97..0000000000 --- a/crates/gpui_macros/src/test.rs +++ /dev/null @@ -1,330 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::Ident; -use quote::{format_ident, quote}; -use std::mem; -use syn::{ - self, Expr, ExprLit, FnArg, ItemFn, Lit, Meta, MetaList, PathSegment, Token, Type, - parse::{Parse, ParseStream}, - parse_quote, - punctuated::Punctuated, - spanned::Spanned, -}; - -struct Args { - seeds: Vec, - max_retries: usize, - max_iterations: usize, - on_failure_fn_name: proc_macro2::TokenStream, -} - -impl Parse for Args { - fn parse(input: ParseStream) -> Result { - let mut seeds = Vec::::new(); - let mut max_retries = 0; - let mut max_iterations = 1; - let mut on_failure_fn_name = quote!(None); - - let metas = Punctuated::::parse_terminated(input)?; - - for meta in metas { - let ident = { - let meta_path = match &meta { - Meta::NameValue(meta) => &meta.path, - Meta::List(list) => &list.path, - Meta::Path(path) => { - return Err(syn::Error::new(path.span(), "invalid path argument")); - } - }; - let Some(ident) = meta_path.get_ident() else { - return Err(syn::Error::new(meta_path.span(), "unexpected path")); - }; - ident.to_string() - }; - - match (&meta, ident.as_str()) { - (Meta::NameValue(meta), "retries") => { - max_retries = parse_usize_from_expr(&meta.value)? - } - (Meta::NameValue(meta), "iterations") => { - max_iterations = parse_usize_from_expr(&meta.value)? - } - (Meta::NameValue(meta), "on_failure") => { - let Expr::Lit(ExprLit { - lit: Lit::Str(name), - .. - }) = &meta.value - else { - return Err(syn::Error::new( - meta.value.span(), - "on_failure argument must be a string", - )); - }; - let segments = name - .value() - .split("::") - .map(|part| PathSegment::from(Ident::new(part, name.span()))) - .collect(); - let path = syn::Path { - leading_colon: None, - segments, - }; - on_failure_fn_name = quote!(Some(#path)); - } - (Meta::NameValue(meta), "seed") => { - seeds = vec![parse_usize_from_expr(&meta.value)? as u64] - } - (Meta::List(list), "seeds") => seeds = parse_u64_array(list)?, - (Meta::Path(_), _) => { - return Err(syn::Error::new(meta.span(), "invalid path argument")); - } - (_, _) => { - return Err(syn::Error::new(meta.span(), "invalid argument name")); - } - } - } - - Ok(Args { - seeds, - max_retries, - max_iterations, - on_failure_fn_name, - }) - } -} - -pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { - let args = syn::parse_macro_input!(args as Args); - let mut inner_fn = match syn::parse::(function) { - Ok(f) => f, - Err(err) => return error_to_stream(err), - }; - - let inner_fn_attributes = mem::take(&mut inner_fn.attrs); - let inner_fn_name = format_ident!("__{}", inner_fn.sig.ident); - let outer_fn_name = mem::replace(&mut inner_fn.sig.ident, inner_fn_name.clone()); - - let result = generate_test_function( - args, - inner_fn, - inner_fn_attributes, - inner_fn_name, - outer_fn_name, - ); - match result { - Ok(tokens) => tokens, - Err(tokens) => tokens, - } -} - -fn generate_test_function( - args: Args, - inner_fn: ItemFn, - inner_fn_attributes: Vec, - inner_fn_name: Ident, - outer_fn_name: Ident, -) -> Result { - let seeds = &args.seeds; - let max_retries = args.max_retries; - let num_iterations = args.max_iterations; - let on_failure_fn_name = &args.on_failure_fn_name; - let seeds = quote!( #(#seeds),* ); - - let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() { - // Pass to the test function the number of app contexts that it needs, - // based on its parameter list. - let mut cx_vars = proc_macro2::TokenStream::new(); - let mut cx_teardowns = proc_macro2::TokenStream::new(); - let mut inner_fn_args = proc_macro2::TokenStream::new(); - for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { - if let FnArg::Typed(arg) = arg { - if let Type::Path(ty) = &*arg.ty { - let last_segment = ty.path.segments.last(); - match last_segment.map(|s| s.ident.to_string()).as_deref() { - Some("StdRng") => { - inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); - continue; - } - Some("BackgroundExecutor") => { - inner_fn_args.extend(quote!(gpui::BackgroundExecutor::new( - std::sync::Arc::new(dispatcher.clone()), - ),)); - continue; - } - _ => {} - } - } else if let Type::Reference(ty) = &*arg.ty - && let Type::Path(ty) = &*ty.elem - { - let last_segment = ty.path.segments.last(); - if let Some("TestAppContext") = - last_segment.map(|s| s.ident.to_string()).as_deref() - { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)), - ); - )); - cx_teardowns.extend(quote!( - dispatcher.run_until_parked(); - #cx_varname.executor().forbid_parking(); - #cx_varname.quit(); - dispatcher.run_until_parked(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } - } - } - - return Err(error_with_message("invalid function signature", arg)); - } - - parse_quote! { - #[test] - fn #outer_fn_name() { - #inner_fn - - gpui::run_test( - #num_iterations, - &[#seeds], - #max_retries, - &mut |dispatcher, _seed| { - let executor = gpui::BackgroundExecutor::new(std::sync::Arc::new(dispatcher.clone())); - #cx_vars - executor.block_test(#inner_fn_name(#inner_fn_args)); - #cx_teardowns - }, - #on_failure_fn_name - ); - } - } - } else { - // Pass to the test function the number of app contexts that it needs, - // based on its parameter list. - let mut cx_vars = proc_macro2::TokenStream::new(); - let mut cx_teardowns = proc_macro2::TokenStream::new(); - let mut inner_fn_args = proc_macro2::TokenStream::new(); - for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() { - if let FnArg::Typed(arg) = arg { - if let Type::Path(ty) = &*arg.ty { - let last_segment = ty.path.segments.last(); - - if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() { - inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),)); - continue; - } - } else if let Type::Reference(ty) = &*arg.ty - && let Type::Path(ty) = &*ty.elem - { - let last_segment = ty.path.segments.last(); - match last_segment.map(|s| s.ident.to_string()).as_deref() { - Some("App") => { - let cx_varname = format_ident!("cx_{}", ix); - let cx_varname_lock = format_ident!("cx_{}_lock", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); - cx_teardowns.extend(quote!( - drop(#cx_varname_lock); - dispatcher.run_until_parked(); - #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); - dispatcher.run_until_parked(); - )); - continue; - } - Some("TestAppContext") => { - let cx_varname = format_ident!("cx_{}", ix); - cx_vars.extend(quote!( - let mut #cx_varname = gpui::TestAppContext::build( - dispatcher.clone(), - Some(stringify!(#outer_fn_name)) - ); - )); - cx_teardowns.extend(quote!( - dispatcher.run_until_parked(); - #cx_varname.executor().forbid_parking(); - #cx_varname.quit(); - dispatcher.run_until_parked(); - )); - inner_fn_args.extend(quote!(&mut #cx_varname,)); - continue; - } - _ => {} - } - } - } - - return Err(error_with_message("invalid function signature", arg)); - } - - parse_quote! { - #[test] - fn #outer_fn_name() { - #inner_fn - - gpui::run_test( - #num_iterations, - &[#seeds], - #max_retries, - &mut |dispatcher, _seed| { - #cx_vars - #inner_fn_name(#inner_fn_args); - #cx_teardowns - }, - #on_failure_fn_name, - ); - } - } - }; - outer_fn.attrs.extend(inner_fn_attributes); - - Ok(TokenStream::from(quote!(#outer_fn))) -} - -fn parse_usize_from_expr(expr: &Expr) -> Result { - let Expr::Lit(ExprLit { - lit: Lit::Int(int), .. - }) = expr - else { - return Err(syn::Error::new(expr.span(), "expected an integer")); - }; - int.base10_parse() - .map_err(|_| syn::Error::new(int.span(), "failed to parse integer")) -} - -fn parse_u64_array(meta_list: &MetaList) -> Result, syn::Error> { - let mut result = Vec::new(); - let tokens = &meta_list.tokens; - let parser = |input: ParseStream| { - let exprs = Punctuated::::parse_terminated(input)?; - for expr in exprs { - if let Expr::Lit(ExprLit { - lit: Lit::Int(int), .. - }) = expr - { - let value: usize = int.base10_parse()?; - result.push(value as u64); - } else { - return Err(syn::Error::new(expr.span(), "expected an integer")); - } - } - Ok(()) - }; - syn::parse::Parser::parse2(parser, tokens.clone())?; - Ok(result) -} - -fn error_with_message(message: &str, spanned: impl Spanned) -> TokenStream { - error_to_stream(syn::Error::new(spanned.span(), message)) -} - -fn error_to_stream(err: syn::Error) -> TokenStream { - TokenStream::from(err.into_compile_error()) -} diff --git a/crates/gpui_macros/tests/derive_context.rs b/crates/gpui_macros/tests/derive_context.rs deleted file mode 100644 index 6c122eff25..0000000000 --- a/crates/gpui_macros/tests/derive_context.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[test] -fn test_derive_context() { - use gpui::{App, Window}; - use gpui_macros::{AppContext, VisualContext}; - - #[derive(AppContext, VisualContext)] - struct _MyCustomContext<'a, 'b> { - #[app] - app: &'a mut App, - #[window] - window: &'b mut Window, - } -} diff --git a/crates/gpui_macros/tests/derive_inspector_reflection.rs b/crates/gpui_macros/tests/derive_inspector_reflection.rs deleted file mode 100644 index 92f4e56e9c..0000000000 --- a/crates/gpui_macros/tests/derive_inspector_reflection.rs +++ /dev/null @@ -1,133 +0,0 @@ -//! This code was generated using Zed Agent with Claude Opus 4. - -// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv -#[cfg_attr(not(rust_analyzer), gpui_macros::derive_inspector_reflection)] -trait Transform: Clone { - /// Doubles the value - fn double(self) -> Self; - - /// Triples the value - fn triple(self) -> Self; - - /// Increments the value by one - /// - /// This method has a default implementation - fn increment(self) -> Self { - // Default implementation - self.add_one() - } - - /// Quadruples the value by doubling twice - fn quadruple(self) -> Self { - // Default implementation with mut self - self.double().double() - } - - // These methods will be filtered out: - #[allow(dead_code)] - fn add(&self, other: &Self) -> Self; - #[allow(dead_code)] - fn set_value(&mut self, value: i32); - #[allow(dead_code)] - fn get_value(&self) -> i32; - - /// Adds one to the value - fn add_one(self) -> Self; -} - -#[derive(Debug, Clone, PartialEq)] -struct Number(i32); - -impl Transform for Number { - fn double(self) -> Self { - Number(self.0 * 2) - } - - fn triple(self) -> Self { - Number(self.0 * 3) - } - - fn add(&self, other: &Self) -> Self { - Number(self.0 + other.0) - } - - fn set_value(&mut self, value: i32) { - self.0 = value; - } - - fn get_value(&self) -> i32 { - self.0 - } - - fn add_one(self) -> Self { - Number(self.0 + 1) - } -} - -#[test] -fn test_derive_inspector_reflection() { - use transform_reflection::*; - - // Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self - let methods = methods::(); - - assert_eq!(methods.len(), 5); - let method_names: Vec<_> = methods.iter().map(|m| m.name).collect(); - assert!(method_names.contains(&"double")); - assert!(method_names.contains(&"triple")); - assert!(method_names.contains(&"increment")); - assert!(method_names.contains(&"quadruple")); - assert!(method_names.contains(&"add_one")); - - // Invoke methods by name - let num = Number(5); - - let doubled = find_method::("double").unwrap().invoke(num.clone()); - assert_eq!(doubled, Number(10)); - - let tripled = find_method::("triple").unwrap().invoke(num.clone()); - assert_eq!(tripled, Number(15)); - - let incremented = find_method::("increment") - .unwrap() - .invoke(num.clone()); - assert_eq!(incremented, Number(6)); - - let quadrupled = find_method::("quadruple").unwrap().invoke(num); - assert_eq!(quadrupled, Number(20)); - - // Try to invoke a non-existent method - let result = find_method::("nonexistent"); - assert!(result.is_none()); - - // Chain operations - let num = Number(10); - let result = find_method::("double") - .map(|m| m.invoke(num)) - .and_then(|n| find_method::("increment").map(|m| m.invoke(n))) - .and_then(|n| find_method::("triple").map(|m| m.invoke(n))); - - assert_eq!(result, Some(Number(63))); // (10 * 2 + 1) * 3 = 63 - - // Test documentationumentation capture - let double_method = find_method::("double").unwrap(); - assert_eq!(double_method.documentation, Some("Doubles the value")); - - let triple_method = find_method::("triple").unwrap(); - assert_eq!(triple_method.documentation, Some("Triples the value")); - - let increment_method = find_method::("increment").unwrap(); - assert_eq!( - increment_method.documentation, - Some("Increments the value by one\n\nThis method has a default implementation") - ); - - let quadruple_method = find_method::("quadruple").unwrap(); - assert_eq!( - quadruple_method.documentation, - Some("Quadruples the value by doubling twice") - ); - - let add_one_method = find_method::("add_one").unwrap(); - assert_eq!(add_one_method.documentation, Some("Adds one to the value")); -} diff --git a/crates/gpui_macros/tests/render_test.rs b/crates/gpui_macros/tests/render_test.rs deleted file mode 100644 index 98c9062381..0000000000 --- a/crates/gpui_macros/tests/render_test.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[test] -fn test_derive_render() { - use gpui_macros::Render; - - #[derive(Render)] - struct _Element; -} diff --git a/crates/gpui_tokio/Cargo.toml b/crates/gpui_tokio/Cargo.toml deleted file mode 100644 index e9d72b8ec2..0000000000 --- a/crates/gpui_tokio/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "gpui_tokio" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/gpui_tokio.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -util.workspace = true -gpui.workspace = true -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } diff --git a/crates/gpui_tokio/LICENSE-APACHE b/crates/gpui_tokio/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/gpui_tokio/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/gpui_tokio/src/gpui_tokio.rs b/crates/gpui_tokio/src/gpui_tokio.rs deleted file mode 100644 index 61dcfc48ef..0000000000 --- a/crates/gpui_tokio/src/gpui_tokio.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::future::Future; - -use gpui::{App, AppContext, Global, ReadGlobal, Task}; -use util::defer; - -pub use tokio::task::JoinError; - -pub fn init(cx: &mut App) { - cx.set_global(GlobalTokio::new()); -} - -struct GlobalTokio { - runtime: tokio::runtime::Runtime, -} - -impl Global for GlobalTokio {} - -impl GlobalTokio { - fn new() -> Self { - let runtime = tokio::runtime::Builder::new_multi_thread() - // Since we now have two executors, let's try to keep our footprint small - .worker_threads(2) - .enable_all() - .build() - .expect("Failed to initialize Tokio"); - - Self { runtime } - } -} - -pub struct Tokio {} - -impl Tokio { - /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task - /// Note that the Tokio task will be cancelled if the GPUI task is dropped - pub fn spawn(cx: &C, f: Fut) -> C::Result>> - where - C: AppContext, - Fut: Future + Send + 'static, - R: Send + 'static, - { - cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.runtime.spawn(f); - let abort_handle = join_handle.abort_handle(); - let cancel = defer(move || { - abort_handle.abort(); - }); - cx.background_spawn(async move { - let result = join_handle.await; - drop(cancel); - result - }) - }) - } - - /// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task - /// Note that the Tokio task will be cancelled if the GPUI task is dropped - pub fn spawn_result(cx: &C, f: Fut) -> C::Result>> - where - C: AppContext, - Fut: Future> + Send + 'static, - R: Send + 'static, - { - cx.read_global(|tokio: &GlobalTokio, cx| { - let join_handle = tokio.runtime.spawn(f); - let abort_handle = join_handle.abort_handle(); - let cancel = defer(move || { - abort_handle.abort(); - }); - cx.background_spawn(async move { - let result = join_handle.await?; - drop(cancel); - result - }) - }) - } - - pub fn handle(cx: &App) -> tokio::runtime::Handle { - GlobalTokio::global(cx).runtime.handle().clone() - } -} diff --git a/crates/html_to_markdown/Cargo.toml b/crates/html_to_markdown/Cargo.toml deleted file mode 100644 index 70ff3b3555..0000000000 --- a/crates/html_to_markdown/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "html_to_markdown" -version = "0.1.0" -description = "Convert HTML to Markdown" -repository = "https://github.com/zed-industries/zed" -documentation = "https://docs.rs/html_to_markdown" -keywords = ["html", "markdown", "html-to-markdown"] -edition.workspace = true -publish = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/html_to_markdown.rs" - -[dependencies] -anyhow.workspace = true -html5ever.workspace = true -markup5ever_rcdom.workspace = true -regex.workspace = true - -[dev-dependencies] -indoc.workspace = true -pretty_assertions.workspace = true diff --git a/crates/html_to_markdown/LICENSE-APACHE b/crates/html_to_markdown/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/html_to_markdown/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/html_to_markdown/src/html_element.rs b/crates/html_to_markdown/src/html_element.rs deleted file mode 100644 index 26918e3c74..0000000000 --- a/crates/html_to_markdown/src/html_element.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::cell::RefCell; -use std::collections::HashSet; -use std::sync::OnceLock; - -use html5ever::Attribute; - -/// Returns a [`HashSet`] containing the HTML elements that are inline by default. -/// -/// [MDN: List of "inline" elements](https://yari-demos.prod.mdn.mozit.cloud/en-US/docs/Web/HTML/Inline_elements) -fn inline_elements() -> &'static HashSet<&'static str> { - static INLINE_ELEMENTS: OnceLock> = OnceLock::new(); - INLINE_ELEMENTS.get_or_init(|| { - HashSet::from_iter([ - "a", "abbr", "acronym", "audio", "b", "bdi", "bdo", "big", "br", "button", "canvas", - "cite", "code", "data", "datalist", "del", "dfn", "em", "embed", "i", "iframe", "img", - "input", "ins", "kbd", "label", "map", "mark", "meter", "noscript", "object", "output", - "picture", "progress", "q", "ruby", "s", "samp", "script", "select", "slot", "small", - "span", "strong", "sub", "sup", "svg", "template", "textarea", "time", "tt", "u", - "var", "video", "wbr", - ]) - }) -} - -#[derive(Debug, Clone)] -pub struct HtmlElement { - tag: String, - pub(crate) attrs: RefCell>, -} - -impl HtmlElement { - pub fn new(tag: String, attrs: RefCell>) -> Self { - Self { tag, attrs } - } - - pub fn tag(&self) -> &str { - &self.tag - } - - /// Returns whether this [`HtmlElement`] is an inline element. - pub fn is_inline(&self) -> bool { - inline_elements().contains(self.tag.as_str()) - } - - /// Returns the attribute with the specified name. - pub fn attr(&self, name: &str) -> Option { - self.attrs - .borrow() - .iter() - .find(|attr| attr.name.local.to_string() == name) - .map(|attr| attr.value.to_string()) - } - - /// Returns the list of classes on this [`HtmlElement`]. - pub fn classes(&self) -> Vec { - self.attrs - .borrow() - .iter() - .find(|attr| attr.name.local.to_string() == "class") - .map(|attr| { - attr.value - .split(' ') - .map(|class| class.trim().to_string()) - .collect::>() - }) - .unwrap_or_default() - } - - /// Returns whether this [`HtmlElement`] has the specified class. - pub fn has_class(&self, class: &str) -> bool { - self.has_any_classes(&[class]) - } - - /// Returns whether this [`HtmlElement`] has any of the specified classes. - pub fn has_any_classes(&self, classes: &[&str]) -> bool { - self.attrs.borrow().iter().any(|attr| { - attr.name.local.to_string() == "class" - && attr - .value - .split(' ') - .any(|class| classes.contains(&class.trim())) - }) - } -} diff --git a/crates/html_to_markdown/src/html_to_markdown.rs b/crates/html_to_markdown/src/html_to_markdown.rs deleted file mode 100644 index fda2cce5f9..0000000000 --- a/crates/html_to_markdown/src/html_to_markdown.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Convert HTML to Markdown. - -mod html_element; -pub mod markdown; -mod markdown_writer; -pub mod structure; - -use std::io::Read; - -use anyhow::{Context as _, Result}; -use html5ever::driver::ParseOpts; -use html5ever::parse_document; -use html5ever::tendril::TendrilSink; -use html5ever::tree_builder::TreeBuilderOpts; -use markup5ever_rcdom::RcDom; - -pub use crate::html_element::*; -pub use crate::markdown_writer::*; - -/// Converts the provided HTML to Markdown. -pub fn convert_html_to_markdown(html: impl Read, handlers: &mut [TagHandler]) -> Result { - let dom = parse_html(html).context("failed to parse HTML")?; - - let markdown_writer = MarkdownWriter::new(); - let markdown = markdown_writer - .run(&dom.document, handlers) - .context("failed to convert HTML to Markdown")?; - - Ok(markdown) -} - -fn parse_html(mut html: impl Read) -> Result { - let parse_options = ParseOpts { - tree_builder: TreeBuilderOpts { - drop_doctype: true, - ..Default::default() - }, - ..Default::default() - }; - let dom = parse_document(RcDom::default(), parse_options) - .from_utf8() - .read_from(&mut html) - .context("failed to parse HTML document")?; - - Ok(dom) -} diff --git a/crates/html_to_markdown/src/markdown.rs b/crates/html_to_markdown/src/markdown.rs deleted file mode 100644 index bb3b3563bc..0000000000 --- a/crates/html_to_markdown/src/markdown.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::html_element::HtmlElement; -use crate::markdown_writer::{HandleTag, HandlerOutcome, MarkdownWriter, StartTagOutcome}; - -pub struct WebpageChromeRemover; - -impl HandleTag for WebpageChromeRemover { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "head" | "script" | "style" | "nav") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - _writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "head" | "script" | "style" | "nav" => return StartTagOutcome::Skip, - _ => {} - } - - StartTagOutcome::Continue - } -} - -pub struct ParagraphHandler; - -impl HandleTag for ParagraphHandler { - fn should_handle(&self, _tag: &str) -> bool { - true - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - if tag.is_inline() - && writer.is_inside("p") - && let Some(parent) = writer.current_element_stack().iter().last() - && !(parent.is_inline() - || writer.markdown.ends_with(' ') - || writer.markdown.ends_with('\n')) - { - writer.push_str(" "); - } - - if tag.tag() == "p" { - writer.push_blank_line() - } - StartTagOutcome::Continue - } -} - -pub struct HeadingHandler; - -impl HandleTag for HeadingHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "h1" | "h2" | "h3" | "h4" | "h5" | "h6") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "h1" => writer.push_str("\n\n# "), - "h2" => writer.push_str("\n\n## "), - "h3" => writer.push_str("\n\n### "), - "h4" => writer.push_str("\n\n#### "), - "h5" => writer.push_str("\n\n##### "), - "h6" => writer.push_str("\n\n###### "), - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => writer.push_blank_line(), - _ => {} - } - } -} - -pub struct ListHandler; - -impl HandleTag for ListHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "ul" | "ol" | "li") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "ul" | "ol" => writer.push_newline(), - "li" => writer.push_str("- "), - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "ul" | "ol" => writer.push_newline(), - "li" => writer.push_newline(), - _ => {} - } - } -} - -pub struct TableHandler { - /// The number of columns in the current ``. - current_table_columns: usize, - is_first_th: bool, - is_first_td: bool, -} - -impl TableHandler { - pub fn new() -> Self { - Self { - current_table_columns: 0, - is_first_th: true, - is_first_td: true, - } - } -} - -impl Default for TableHandler { - fn default() -> Self { - Self::new() - } -} - -impl HandleTag for TableHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "table" | "thead" | "tbody" | "tr" | "th" | "td") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "thead" => writer.push_blank_line(), - "tr" => writer.push_newline(), - "th" => { - self.current_table_columns += 1; - if self.is_first_th { - self.is_first_th = false; - } else { - writer.push_str(" "); - } - writer.push_str("| "); - } - "td" => { - if self.is_first_td { - self.is_first_td = false; - } else { - writer.push_str(" "); - } - writer.push_str("| "); - } - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "thead" => { - writer.push_newline(); - for ix in 0..self.current_table_columns { - if ix > 0 { - writer.push_str(" "); - } - writer.push_str("| ---"); - } - writer.push_str(" |"); - self.is_first_th = true; - } - "tr" => { - writer.push_str(" |"); - self.is_first_td = true; - } - "table" => { - self.current_table_columns = 0; - } - _ => {} - } - } -} - -pub struct StyledTextHandler; - -impl HandleTag for StyledTextHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "strong" | "em") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "strong" => writer.push_str("**"), - "em" => writer.push_str("_"), - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "strong" => writer.push_str("**"), - "em" => writer.push_str("_"), - _ => {} - } - } -} - -pub struct CodeHandler; - -impl HandleTag for CodeHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "pre" | "code") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "pre" => writer.push_str("\n\n```\n"), - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "pre" => writer.push_str("\n```\n"), - _ => {} - } - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if writer.is_inside("pre") { - writer.push_str(text); - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} diff --git a/crates/html_to_markdown/src/markdown_writer.rs b/crates/html_to_markdown/src/markdown_writer.rs deleted file mode 100644 index c32205ae7b..0000000000 --- a/crates/html_to_markdown/src/markdown_writer.rs +++ /dev/null @@ -1,199 +0,0 @@ -use std::collections::VecDeque; -use std::rc::Rc; -use std::{cell::RefCell, sync::LazyLock}; - -use anyhow::Result; -use markup5ever_rcdom::{Handle, NodeData}; -use regex::Regex; - -use crate::html_element::HtmlElement; - -fn empty_line_regex() -> &'static Regex { - static REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^\s*$").expect("Failed to create empty_line_regex")); - ®EX -} - -fn more_than_three_newlines_regex() -> &'static Regex { - static REGEX: LazyLock = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap()); - ®EX -} - -pub enum StartTagOutcome { - Continue, - Skip, -} - -pub type TagHandler = Rc>; - -pub struct MarkdownWriter { - current_element_stack: VecDeque, - pub(crate) markdown: String, -} - -impl Default for MarkdownWriter { - fn default() -> Self { - Self::new() - } -} - -impl MarkdownWriter { - pub fn new() -> Self { - Self { - current_element_stack: VecDeque::new(), - markdown: String::new(), - } - } - - pub fn current_element_stack(&self) -> &VecDeque { - &self.current_element_stack - } - - pub fn is_inside(&self, tag: &str) -> bool { - self.current_element_stack - .iter() - .any(|parent_element| parent_element.tag() == tag) - } - - /// Appends the given string slice onto the end of the Markdown output. - pub fn push_str(&mut self, str: &str) { - self.markdown.push_str(str); - } - - /// Appends a newline to the end of the Markdown output. - pub fn push_newline(&mut self) { - self.push_str("\n"); - } - - /// Appends a blank line to the end of the Markdown output. - pub fn push_blank_line(&mut self) { - self.push_str("\n\n"); - } - - pub fn run(mut self, root_node: &Handle, handlers: &mut [TagHandler]) -> Result { - self.visit_node(root_node, handlers)?; - Ok(Self::prettify_markdown(self.markdown)) - } - - fn prettify_markdown(markdown: String) -> String { - let markdown = empty_line_regex().replace_all(&markdown, ""); - let markdown = more_than_three_newlines_regex().replace_all(&markdown, "\n\n"); - - markdown.trim().to_string() - } - - fn visit_node(&mut self, node: &Handle, handlers: &mut [TagHandler]) -> Result<()> { - let mut current_element = None; - - match node.data { - NodeData::Document - | NodeData::Doctype { .. } - | NodeData::ProcessingInstruction { .. } - | NodeData::Comment { .. } => { - // Currently left unimplemented, as we're not interested in this data - // at this time. - } - NodeData::Element { - ref name, - ref attrs, - .. - } => { - let tag_name = name.local.to_string(); - if !tag_name.is_empty() { - current_element = Some(HtmlElement::new(tag_name, attrs.clone())); - } - } - NodeData::Text { ref contents } => { - let text = contents.borrow().to_string(); - self.visit_text(text, handlers)?; - } - } - - if let Some(current_element) = current_element.as_ref() { - match self.start_tag(current_element, handlers) { - StartTagOutcome::Continue => {} - StartTagOutcome::Skip => return Ok(()), - } - - self.current_element_stack - .push_back(current_element.clone()); - } - - if self.current_element_stack.len() < 200 { - for child in node.children.borrow().iter() { - self.visit_node(child, handlers)?; - } - } - - if let Some(current_element) = current_element { - self.current_element_stack.pop_back(); - self.end_tag(¤t_element, handlers); - } - - Ok(()) - } - - fn start_tag(&mut self, tag: &HtmlElement, handlers: &mut [TagHandler]) -> StartTagOutcome { - for handler in handlers { - if handler.borrow().should_handle(tag.tag()) { - match handler.borrow_mut().handle_tag_start(tag, self) { - StartTagOutcome::Continue => {} - StartTagOutcome::Skip => return StartTagOutcome::Skip, - } - } - } - - StartTagOutcome::Continue - } - - fn end_tag(&mut self, tag: &HtmlElement, handlers: &mut [TagHandler]) { - for handler in handlers { - if handler.borrow().should_handle(tag.tag()) { - handler.borrow_mut().handle_tag_end(tag, self); - } - } - } - - fn visit_text(&mut self, text: String, handlers: &mut [TagHandler]) -> Result<()> { - for handler in handlers { - match handler.borrow_mut().handle_text(&text, self) { - HandlerOutcome::Handled => return Ok(()), - HandlerOutcome::NoOp => {} - } - } - - let text = text - .trim_matches(|char| char == '\n' || char == '\r' || char == '\t') - .replace('\n', " "); - - self.push_str(&text); - - Ok(()) - } -} - -pub enum HandlerOutcome { - Handled, - NoOp, -} - -pub trait HandleTag { - /// Returns whether this handler should handle the given tag. - fn should_handle(&self, tag: &str) -> bool; - - /// Handles the start of the given tag. - fn handle_tag_start( - &mut self, - _tag: &HtmlElement, - _writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - StartTagOutcome::Continue - } - - /// Handles the end of the given tag. - fn handle_tag_end(&mut self, _tag: &HtmlElement, _writer: &mut MarkdownWriter) {} - - fn handle_text(&mut self, _text: &str, _writer: &mut MarkdownWriter) -> HandlerOutcome { - HandlerOutcome::NoOp - } -} diff --git a/crates/html_to_markdown/src/structure.rs b/crates/html_to_markdown/src/structure.rs deleted file mode 100644 index 622d63c774..0000000000 --- a/crates/html_to_markdown/src/structure.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod wikipedia; diff --git a/crates/html_to_markdown/src/structure/wikipedia.rs b/crates/html_to_markdown/src/structure/wikipedia.rs deleted file mode 100644 index c8d399a661..0000000000 --- a/crates/html_to_markdown/src/structure/wikipedia.rs +++ /dev/null @@ -1,181 +0,0 @@ -use crate::HandleTag; -use crate::html_element::HtmlElement; -use crate::markdown_writer::{HandlerOutcome, MarkdownWriter, StartTagOutcome}; - -pub struct WikipediaChromeRemover; - -impl HandleTag for WikipediaChromeRemover { - fn should_handle(&self, _tag: &str) -> bool { - true - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - _writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "head" | "script" | "style" | "nav" => return StartTagOutcome::Skip, - "sup" => { - if tag.has_class("reference") { - return StartTagOutcome::Skip; - } - } - "div" | "span" | "a" => { - if tag.attr("id").as_deref() == Some("p-lang-btn") { - return StartTagOutcome::Skip; - } - - if tag.attr("id").as_deref() == Some("p-search") { - return StartTagOutcome::Skip; - } - - let classes_to_skip = ["noprint", "mw-editsection", "mw-jump-link"]; - if tag.has_any_classes(&classes_to_skip) { - return StartTagOutcome::Skip; - } - } - _ => {} - } - - StartTagOutcome::Continue - } -} - -pub struct WikipediaInfoboxHandler; - -impl HandleTag for WikipediaInfoboxHandler { - fn should_handle(&self, tag: &str) -> bool { - tag == "table" - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - _writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - if tag.tag() == "table" && tag.has_class("infobox") { - return StartTagOutcome::Skip; - } - - StartTagOutcome::Continue - } -} - -pub struct WikipediaCodeHandler { - language: Option, -} - -impl WikipediaCodeHandler { - pub fn new() -> Self { - Self { language: None } - } -} - -impl Default for WikipediaCodeHandler { - fn default() -> Self { - Self::new() - } -} - -impl HandleTag for WikipediaCodeHandler { - fn should_handle(&self, tag: &str) -> bool { - matches!(tag, "div" | "pre" | "code") - } - - fn handle_tag_start( - &mut self, - tag: &HtmlElement, - writer: &mut MarkdownWriter, - ) -> StartTagOutcome { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "div" => { - let classes = tag.classes(); - self.language = classes.iter().find_map(|class| { - if let Some((_, language)) = class.split_once("mw-highlight-lang-") { - Some(language.trim().to_owned()) - } else { - None - } - }); - } - "pre" => { - writer.push_blank_line(); - writer.push_str("```"); - if let Some(language) = self.language.take() { - writer.push_str(&language); - } - writer.push_newline(); - } - _ => {} - } - - StartTagOutcome::Continue - } - - fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) { - match tag.tag() { - "code" => { - if !writer.is_inside("pre") { - writer.push_str("`"); - } - } - "pre" => writer.push_str("\n```\n"), - _ => {} - } - } - - fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome { - if writer.is_inside("pre") { - writer.push_str(text); - return HandlerOutcome::Handled; - } - - HandlerOutcome::NoOp - } -} - -#[cfg(test)] -mod tests { - use std::cell::RefCell; - use std::rc::Rc; - - use indoc::indoc; - use pretty_assertions::assert_eq; - - use crate::{TagHandler, convert_html_to_markdown, markdown}; - - use super::*; - - fn wikipedia_handlers() -> Vec { - vec![ - Rc::new(RefCell::new(markdown::ParagraphHandler)), - Rc::new(RefCell::new(markdown::HeadingHandler)), - Rc::new(RefCell::new(markdown::ListHandler)), - Rc::new(RefCell::new(markdown::StyledTextHandler)), - Rc::new(RefCell::new(WikipediaChromeRemover)), - ] - } - - #[test] - fn test_citation_references_get_removed() { - let html = indoc! {r##" -

Rust began as a personal project in 2006 by Mozilla Research employee Graydon Hoare.[20] Mozilla began sponsoring the project in 2009 as a part of the ongoing development of an experimental browser engine called Servo,[21] which was officially announced by Mozilla in 2010.[22][23] Rust's memory and ownership system was influenced by region-based memory management in languages such as Cyclone and ML Kit.[5] -

- "##}; - let expected = indoc! {" - Rust began as a personal project in 2006 by Mozilla Research employee Graydon Hoare. Mozilla began sponsoring the project in 2009 as a part of the ongoing development of an experimental browser engine called Servo, which was officially announced by Mozilla in 2010. Rust's memory and ownership system was influenced by region-based memory management in languages such as Cyclone and ML Kit. - "} - .trim(); - - assert_eq!( - convert_html_to_markdown(html.as_bytes(), &mut wikipedia_handlers()).unwrap(), - expected - ) - } -} diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml deleted file mode 100644 index 177f8639ca..0000000000 --- a/crates/http_client/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "http_client" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "A HTTP client library for Zed and GPUI" - -[lints] -workspace = true - -[features] -test-support = [] - -[lib] -path = "src/http_client.rs" -doctest = true - -[dependencies] -anyhow.workspace = true -async-compression.workspace = true -async-fs.workspace = true -async-tar.workspace = true -bytes.workspace = true -derive_more.workspace = true -futures.workspace = true -http-body.workspace = true -http.workspace = true -log.workspace = true -parking_lot.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_urlencoded.workspace = true -sha2.workspace = true -tempfile.workspace = true -url.workspace = true -util.workspace = true diff --git a/crates/http_client/LICENSE-APACHE b/crates/http_client/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/http_client/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs deleted file mode 100644 index 8fb49f2185..0000000000 --- a/crates/http_client/src/async_body.rs +++ /dev/null @@ -1,138 +0,0 @@ -use std::{ - io::{Cursor, Read}, - pin::Pin, - task::Poll, -}; - -use bytes::Bytes; -use futures::AsyncRead; -use http_body::{Body, Frame}; - -/// Based on the implementation of AsyncBody in -/// . -pub struct AsyncBody(pub Inner); - -pub enum Inner { - /// An empty body. - Empty, - - /// A body stored in memory. - Bytes(std::io::Cursor), - - /// An asynchronous reader. - AsyncReader(Pin>), -} - -impl AsyncBody { - /// Create a new empty body. - /// - /// An empty body represents the *absence* of a body, which is semantically - /// different than the presence of a body of zero length. - pub fn empty() -> Self { - Self(Inner::Empty) - } - /// Create a streaming body that reads from the given reader. - pub fn from_reader(read: R) -> Self - where - R: AsyncRead + Send + Sync + 'static, - { - Self(Inner::AsyncReader(Box::pin(read))) - } - - pub fn from_bytes(bytes: Bytes) -> Self { - Self(Inner::Bytes(Cursor::new(bytes))) - } -} - -impl Default for AsyncBody { - fn default() -> Self { - Self(Inner::Empty) - } -} - -impl From<()> for AsyncBody { - fn from(_: ()) -> Self { - Self(Inner::Empty) - } -} - -impl From for AsyncBody { - fn from(bytes: Bytes) -> Self { - Self::from_bytes(bytes) - } -} - -impl From> for AsyncBody { - fn from(body: Vec) -> Self { - Self::from_bytes(body.into()) - } -} - -impl From for AsyncBody { - fn from(body: String) -> Self { - Self::from_bytes(body.into()) - } -} - -impl From<&'static [u8]> for AsyncBody { - #[inline] - fn from(s: &'static [u8]) -> Self { - Self::from_bytes(Bytes::from_static(s)) - } -} - -impl From<&'static str> for AsyncBody { - #[inline] - fn from(s: &'static str) -> Self { - Self::from_bytes(Bytes::from_static(s.as_bytes())) - } -} - -impl> From> for AsyncBody { - fn from(body: Option) -> Self { - match body { - Some(body) => body.into(), - None => Self::empty(), - } - } -} - -impl futures::AsyncRead for AsyncBody { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &mut [u8], - ) -> std::task::Poll> { - // SAFETY: Standard Enum pin projection - let inner = unsafe { &mut self.get_unchecked_mut().0 }; - match inner { - Inner::Empty => Poll::Ready(Ok(0)), - // Blocking call is over an in-memory buffer - Inner::Bytes(cursor) => Poll::Ready(cursor.read(buf)), - Inner::AsyncReader(async_reader) => { - AsyncRead::poll_read(async_reader.as_mut(), cx, buf) - } - } - } -} - -impl Body for AsyncBody { - type Data = Bytes; - type Error = std::io::Error; - - fn poll_frame( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll, Self::Error>>> { - let mut buffer = vec![0; 8192]; - match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) { - Poll::Ready(Ok(0)) => Poll::Ready(None), - Poll::Ready(Ok(n)) => { - let data = Bytes::copy_from_slice(&buffer[..n]); - Poll::Ready(Some(Ok(Frame::data(data)))) - } - Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))), - Poll::Pending => Poll::Pending, - } - } -} diff --git a/crates/http_client/src/github.rs b/crates/http_client/src/github.rs deleted file mode 100644 index e52e2f1d25..0000000000 --- a/crates/http_client/src/github.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::{HttpClient, HttpRequestExt}; -use anyhow::{Context as _, Result, anyhow, bail}; -use futures::AsyncReadExt; -use http::Request; -use serde::Deserialize; -use std::sync::Arc; -use url::Url; - -const GITHUB_API_URL: &str = "https://api.github.com"; - -pub struct GitHubLspBinaryVersion { - pub name: String, - pub url: String, - pub digest: Option, -} - -#[derive(Deserialize, Debug)] -pub struct GithubRelease { - pub tag_name: String, - #[serde(rename = "prerelease")] - pub pre_release: bool, - pub assets: Vec, - pub tarball_url: String, - pub zipball_url: String, -} - -#[derive(Deserialize, Debug)] -pub struct GithubReleaseAsset { - pub name: String, - pub browser_download_url: String, - pub digest: Option, -} - -pub async fn latest_github_release( - repo_name_with_owner: &str, - require_assets: bool, - pre_release: bool, - http: Arc, -) -> anyhow::Result { - let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases"); - - let request = Request::get(&url) - .follow_redirects(crate::RedirectPolicy::FollowAll) - .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| { - builder.header("Authorization", format!("Bearer {}", token)) - }) - .body(Default::default())?; - - let mut response = http - .send(request) - .await - .context("error fetching latest release")?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading latest release")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let releases = match serde_json::from_slice::>(body.as_slice()) { - Ok(releases) => releases, - - Err(err) => { - log::error!("Error deserializing: {err:?}"); - log::error!( - "GitHub API response text: {:?}", - String::from_utf8_lossy(body.as_slice()) - ); - anyhow::bail!("error deserializing latest release: {err:?}"); - } - }; - - let mut release = releases - .into_iter() - .filter(|release| !require_assets || !release.assets.is_empty()) - .find(|release| release.pre_release == pre_release) - .context("finding a prerelease")?; - release.assets.iter_mut().for_each(|asset| { - if let Some(digest) = &mut asset.digest - && let Some(stripped) = digest.strip_prefix("sha256:") - { - *digest = stripped.to_owned(); - } - }); - Ok(release) -} - -pub async fn get_release_by_tag_name( - repo_name_with_owner: &str, - tag: &str, - http: Arc, -) -> anyhow::Result { - let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}"); - - let request = Request::get(&url) - .follow_redirects(crate::RedirectPolicy::FollowAll) - .when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| { - builder.header("Authorization", format!("Bearer {}", token)) - }) - .body(Default::default())?; - - let mut response = http - .send(request) - .await - .context("error fetching latest release")?; - - let mut body = Vec::new(); - let status = response.status(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading latest release")?; - - if status.is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let release = serde_json::from_slice::(body.as_slice()).map_err(|err| { - log::error!("Error deserializing: {err:?}"); - log::error!( - "GitHub API response text: {:?}", - String::from_utf8_lossy(body.as_slice()) - ); - anyhow!("error deserializing GitHub release: {err:?}") - })?; - - Ok(release) -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum AssetKind { - TarGz, - Gz, - Zip, -} - -pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result { - let mut url = Url::parse(&format!( - "https://github.com/{repo_name_with_owner}/archive/refs/tags", - ))?; - // We're pushing this here, because tags may contain `/` and other characters - // that need to be escaped. - let asset_filename = format!( - "{tag}.{extension}", - extension = match kind { - AssetKind::TarGz => "tar.gz", - AssetKind::Gz => "gz", - AssetKind::Zip => "zip", - } - ); - url.path_segments_mut() - .map_err(|()| anyhow!("cannot modify url path segments"))? - .push(&asset_filename); - Ok(url.to_string()) -} - -#[cfg(test)] -mod tests { - use crate::github::{AssetKind, build_asset_url}; - - #[test] - fn test_build_asset_url() { - let tag = "release/2.3.5"; - let repo_name_with_owner = "microsoft/vscode-eslint"; - - let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap(); - assert_eq!( - tarball, - "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz" - ); - - let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap(); - assert_eq!( - zip, - "https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip" - ); - } -} diff --git a/crates/http_client/src/github_download.rs b/crates/http_client/src/github_download.rs deleted file mode 100644 index 02dee08b21..0000000000 --- a/crates/http_client/src/github_download.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::{path::Path, pin::Pin, task::Poll}; - -use anyhow::{Context, Result}; -use async_compression::futures::bufread::GzipDecoder; -use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader}; -use sha2::{Digest, Sha256}; - -use crate::{HttpClient, github::AssetKind}; - -#[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GithubBinaryMetadata { - pub metadata_version: u64, - pub digest: Option, -} - -impl GithubBinaryMetadata { - pub async fn read_from_file(metadata_path: &Path) -> Result { - let metadata_content = async_fs::read_to_string(metadata_path) - .await - .with_context(|| format!("reading metadata file at {metadata_path:?}"))?; - serde_json::from_str(&metadata_content) - .with_context(|| format!("parsing metadata file at {metadata_path:?}")) - } - - pub async fn write_to_file(&self, metadata_path: &Path) -> Result<()> { - let metadata_content = serde_json::to_string(self) - .with_context(|| format!("serializing metadata for {metadata_path:?}"))?; - async_fs::write(metadata_path, metadata_content.as_bytes()) - .await - .with_context(|| format!("writing metadata file at {metadata_path:?}"))?; - Ok(()) - } -} - -pub async fn download_server_binary( - http_client: &dyn HttpClient, - url: &str, - digest: Option<&str>, - destination_path: &Path, - asset_kind: AssetKind, -) -> Result<(), anyhow::Error> { - log::info!("downloading github artifact from {url}"); - let mut response = http_client - .get(url, Default::default(), true) - .await - .with_context(|| format!("downloading release from {url}"))?; - let body = response.body_mut(); - match digest { - Some(expected_sha_256) => { - let temp_asset_file = tempfile::NamedTempFile::new() - .with_context(|| format!("creating a temporary file for {url}"))?; - let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts(); - let mut writer = HashingWriter { - writer: async_fs::File::from(temp_asset_file), - hasher: Sha256::new(), - }; - futures::io::copy(&mut BufReader::new(body), &mut writer) - .await - .with_context(|| { - format!("saving archive contents into the temporary file for {url}",) - })?; - let asset_sha_256 = format!("{:x}", writer.hasher.finalize()); - - anyhow::ensure!( - asset_sha_256 == expected_sha_256, - "{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}", - ); - writer - .writer - .seek(std::io::SeekFrom::Start(0)) - .await - .with_context(|| format!("seeking temporary file {destination_path:?}",))?; - stream_file_archive(&mut writer.writer, url, destination_path, asset_kind) - .await - .with_context(|| { - format!("extracting downloaded asset for {url} into {destination_path:?}",) - })?; - } - None => stream_response_archive(body, url, destination_path, asset_kind) - .await - .with_context(|| { - format!("extracting response for asset {url} into {destination_path:?}",) - })?, - } - Ok(()) -} - -async fn stream_response_archive( - response: impl AsyncRead + Unpin, - url: &str, - destination_path: &Path, - asset_kind: AssetKind, -) -> Result<()> { - match asset_kind { - AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?, - AssetKind::Gz => extract_gz(destination_path, url, response).await?, - AssetKind::Zip => { - util::archive::extract_zip(destination_path, response).await?; - } - }; - Ok(()) -} - -async fn stream_file_archive( - file_archive: impl AsyncRead + AsyncSeek + Unpin, - url: &str, - destination_path: &Path, - asset_kind: AssetKind, -) -> Result<()> { - match asset_kind { - AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?, - AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?, - #[cfg(not(windows))] - AssetKind::Zip => { - util::archive::extract_seekable_zip(destination_path, file_archive).await?; - } - #[cfg(windows)] - AssetKind::Zip => { - util::archive::extract_zip(destination_path, file_archive).await?; - } - }; - Ok(()) -} - -async fn extract_tar_gz( - destination_path: &Path, - url: &str, - from: impl AsyncRead + Unpin, -) -> Result<(), anyhow::Error> { - let decompressed_bytes = GzipDecoder::new(BufReader::new(from)); - let archive = async_tar::Archive::new(decompressed_bytes); - archive - .unpack(&destination_path) - .await - .with_context(|| format!("extracting {url} to {destination_path:?}"))?; - Ok(()) -} - -async fn extract_gz( - destination_path: &Path, - url: &str, - from: impl AsyncRead + Unpin, -) -> Result<(), anyhow::Error> { - let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from)); - let mut file = async_fs::File::create(&destination_path) - .await - .with_context(|| { - format!("creating a file {destination_path:?} for a download from {url}") - })?; - futures::io::copy(&mut decompressed_bytes, &mut file) - .await - .with_context(|| format!("extracting {url} to {destination_path:?}"))?; - Ok(()) -} - -struct HashingWriter { - writer: W, - hasher: Sha256, -} - -impl AsyncWrite for HashingWriter { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - buf: &[u8], - ) -> Poll> { - match Pin::new(&mut self.writer).poll_write(cx, buf) { - Poll::Ready(Ok(n)) => { - self.hasher.update(&buf[..n]); - Poll::Ready(Ok(n)) - } - other => other, - } - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.writer).poll_flush(cx) - } - - fn poll_close( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - Pin::new(&mut self.writer).poll_close(cx) - } -} diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs deleted file mode 100644 index 2e814ccdc3..0000000000 --- a/crates/http_client/src/http_client.rs +++ /dev/null @@ -1,386 +0,0 @@ -mod async_body; -pub mod github; -pub mod github_download; - -pub use anyhow::{Result, anyhow}; -pub use async_body::{AsyncBody, Inner}; -use derive_more::Deref; -use http::HeaderValue; -pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder}; - -use futures::future::BoxFuture; -use parking_lot::Mutex; - -use std::sync::Arc; -#[cfg(feature = "test-support")] -use std::{any::type_name, fmt}; -pub use url::{Host, Url}; - -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] -pub enum RedirectPolicy { - #[default] - NoFollow, - FollowLimit(u32), - FollowAll, -} -pub struct FollowRedirects(pub bool); - -pub trait HttpRequestExt { - /// Conditionally modify self with the given closure. - fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - if condition { then(self) } else { self } - } - - /// Conditionally unwrap and modify self with the given closure, if the given option is Some. - fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self - where - Self: Sized, - { - match option { - Some(value) => then(self, value), - None => self, - } - } - - /// Whether or not to follow redirects - fn follow_redirects(self, follow: RedirectPolicy) -> Self; -} - -impl HttpRequestExt for http::request::Builder { - fn follow_redirects(self, follow: RedirectPolicy) -> Self { - self.extension(follow) - } -} - -pub trait HttpClient: 'static + Send + Sync { - fn user_agent(&self) -> Option<&HeaderValue>; - - fn proxy(&self) -> Option<&Url>; - - fn send( - &self, - req: http::Request, - ) -> BoxFuture<'static, anyhow::Result>>; - - fn get( - &self, - uri: &str, - body: AsyncBody, - follow_redirects: bool, - ) -> BoxFuture<'static, anyhow::Result>> { - let request = Builder::new() - .uri(uri) - .follow_redirects(if follow_redirects { - RedirectPolicy::FollowAll - } else { - RedirectPolicy::NoFollow - }) - .body(body); - - match request { - Ok(request) => self.send(request), - Err(e) => Box::pin(async move { Err(e.into()) }), - } - } - - fn post_json( - &self, - uri: &str, - body: AsyncBody, - ) -> BoxFuture<'static, anyhow::Result>> { - let request = Builder::new() - .uri(uri) - .method(Method::POST) - .header("Content-Type", "application/json") - .body(body); - - match request { - Ok(request) => self.send(request), - Err(e) => Box::pin(async move { Err(e.into()) }), - } - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - panic!("called as_fake on {}", type_name::()) - } -} - -/// An [`HttpClient`] that may have a proxy. -#[derive(Deref)] -pub struct HttpClientWithProxy { - #[deref] - client: Arc, - proxy: Option, -} - -impl HttpClientWithProxy { - /// Returns a new [`HttpClientWithProxy`] with the given proxy URL. - pub fn new(client: Arc, proxy_url: Option) -> Self { - let proxy_url = proxy_url - .and_then(|proxy| proxy.parse().ok()) - .or_else(read_proxy_from_env); - - Self::new_url(client, proxy_url) - } - pub fn new_url(client: Arc, proxy_url: Option) -> Self { - Self { - client, - proxy: proxy_url, - } - } -} - -impl HttpClient for HttpClientWithProxy { - fn send( - &self, - req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - self.client.send(req) - } - - fn user_agent(&self) -> Option<&HeaderValue> { - self.client.user_agent() - } - - fn proxy(&self) -> Option<&Url> { - self.proxy.as_ref() - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() - } -} - -/// An [`HttpClient`] that has a base URL. -#[derive(Deref)] -pub struct HttpClientWithUrl { - base_url: Mutex, - #[deref] - client: HttpClientWithProxy, -} - -impl HttpClientWithUrl { - /// Returns a new [`HttpClientWithUrl`] with the given base URL. - pub fn new( - client: Arc, - base_url: impl Into, - proxy_url: Option, - ) -> Self { - let client = HttpClientWithProxy::new(client, proxy_url); - - Self { - base_url: Mutex::new(base_url.into()), - client, - } - } - - pub fn new_url( - client: Arc, - base_url: impl Into, - proxy_url: Option, - ) -> Self { - let client = HttpClientWithProxy::new_url(client, proxy_url); - - Self { - base_url: Mutex::new(base_url.into()), - client, - } - } - - /// Returns the base URL. - pub fn base_url(&self) -> String { - self.base_url.lock().clone() - } - - /// Sets the base URL. - pub fn set_base_url(&self, base_url: impl Into) { - let base_url = base_url.into(); - *self.base_url.lock() = base_url; - } - - /// Builds a URL using the given path. - pub fn build_url(&self, path: &str) -> String { - format!("{}{}", self.base_url(), path) - } - -} - -impl HttpClient for HttpClientWithUrl { - fn send( - &self, - req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - self.client.send(req) - } - - fn user_agent(&self) -> Option<&HeaderValue> { - self.client.user_agent() - } - - fn proxy(&self) -> Option<&Url> { - self.client.proxy.as_ref() - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - self.client.as_fake() - } -} - -pub fn read_proxy_from_env() -> Option { - const ENV_VARS: &[&str] = &[ - "ALL_PROXY", - "all_proxy", - "HTTPS_PROXY", - "https_proxy", - "HTTP_PROXY", - "http_proxy", - ]; - - ENV_VARS - .iter() - .find_map(|var| std::env::var(var).ok()) - .and_then(|env| env.parse().ok()) -} - -pub fn read_no_proxy_from_env() -> Option { - const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"]; - - ENV_VARS.iter().find_map(|var| std::env::var(var).ok()) -} - -pub struct BlockedHttpClient; - -impl BlockedHttpClient { - pub fn new() -> Self { - BlockedHttpClient - } -} - -impl HttpClient for BlockedHttpClient { - fn send( - &self, - _req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - Box::pin(async { - Err(std::io::Error::new( - std::io::ErrorKind::PermissionDenied, - "BlockedHttpClient disallowed request", - ) - .into()) - }) - } - - fn user_agent(&self) -> Option<&HeaderValue> { - None - } - - fn proxy(&self) -> Option<&Url> { - None - } - - #[cfg(feature = "test-support")] - fn as_fake(&self) -> &FakeHttpClient { - panic!("called as_fake on {}", type_name::()) - } -} - -#[cfg(feature = "test-support")] -type FakeHttpHandler = Arc< - dyn Fn(Request) -> BoxFuture<'static, anyhow::Result>> - + Send - + Sync - + 'static, ->; - -#[cfg(feature = "test-support")] -pub struct FakeHttpClient { - handler: Mutex>, - user_agent: HeaderValue, -} - -#[cfg(feature = "test-support")] -impl FakeHttpClient { - pub fn create(handler: F) -> Arc - where - Fut: futures::Future>> + Send + 'static, - F: Fn(Request) -> Fut + Send + Sync + 'static, - { - Arc::new(HttpClientWithUrl { - base_url: Mutex::new("http://test.example".into()), - client: HttpClientWithProxy { - client: Arc::new(Self { - handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))), - user_agent: HeaderValue::from_static(type_name::()), - }), - proxy: None, - }, - }) - } - - pub fn with_404_response() -> Arc { - log::warn!("Using fake HTTP client with 404 response"); - Self::create(|_| async move { - Ok(Response::builder() - .status(404) - .body(Default::default()) - .unwrap()) - }) - } - - pub fn with_200_response() -> Arc { - log::warn!("Using fake HTTP client with 200 response"); - Self::create(|_| async move { - Ok(Response::builder() - .status(200) - .body(Default::default()) - .unwrap()) - }) - } - - pub fn replace_handler(&self, new_handler: F) - where - Fut: futures::Future>> + Send + 'static, - F: Fn(FakeHttpHandler, Request) -> Fut + Send + Sync + 'static, - { - let mut handler = self.handler.lock(); - let old_handler = handler.take().unwrap(); - *handler = Some(Arc::new(move |req| { - Box::pin(new_handler(old_handler.clone(), req)) - })); - } -} - -#[cfg(feature = "test-support")] -impl fmt::Debug for FakeHttpClient { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("FakeHttpClient").finish() - } -} - -#[cfg(feature = "test-support")] -impl HttpClient for FakeHttpClient { - fn send( - &self, - req: Request, - ) -> BoxFuture<'static, anyhow::Result>> { - ((self.handler.lock().as_ref().unwrap())(req)) as _ - } - - fn user_agent(&self) -> Option<&HeaderValue> { - Some(&self.user_agent) - } - - fn proxy(&self) -> Option<&Url> { - None - } - - fn as_fake(&self) -> &FakeHttpClient { - self - } -} diff --git a/crates/http_client_tls/Cargo.toml b/crates/http_client_tls/Cargo.toml deleted file mode 100644 index a55268ac31..0000000000 --- a/crates/http_client_tls/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "http_client_tls" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[features] -test-support = [] - -[lib] -path = "src/http_client_tls.rs" -doctest = true - -[dependencies] -rustls.workspace = true -rustls-platform-verifier.workspace = true diff --git a/crates/http_client_tls/LICENSE-APACHE b/crates/http_client_tls/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/http_client_tls/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/http_client_tls/src/http_client_tls.rs b/crates/http_client_tls/src/http_client_tls.rs deleted file mode 100644 index 8ddde5c15a..0000000000 --- a/crates/http_client_tls/src/http_client_tls.rs +++ /dev/null @@ -1,21 +0,0 @@ -use std::sync::OnceLock; - -use rustls::ClientConfig; -use rustls_platform_verifier::ConfigVerifierExt; - -static TLS_CONFIG: OnceLock = OnceLock::new(); - -pub fn tls_config() -> ClientConfig { - TLS_CONFIG - .get_or_init(|| { - // rustls uses the `aws_lc_rs` provider by default - // This only errors if the default provider has already - // been installed. We can ignore this `Result`. - rustls::crypto::aws_lc_rs::default_provider() - .install_default() - .ok(); - - ClientConfig::with_platform_verifier() - }) - .clone() -} diff --git a/crates/media/Cargo.toml b/crates/media/Cargo.toml deleted file mode 100644 index 90a3d93833..0000000000 --- a/crates/media/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "media" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "Bindings to macos media handling APIs for Zed" - -[lints] -workspace = true - -[lib] -path = "src/media.rs" -doctest = false - -[dependencies] -anyhow.workspace = true - -[target.'cfg(target_os = "macos")'.dependencies] -core-foundation.workspace = true -ctor.workspace = true -foreign-types = "0.5" -metal.workspace = true -core-video.workspace = true -objc.workspace = true - -[build-dependencies] -bindgen = "0.71" diff --git a/crates/media/LICENSE-APACHE b/crates/media/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/media/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/media/build.rs b/crates/media/build.rs deleted file mode 100644 index 090002e9e9..0000000000 --- a/crates/media/build.rs +++ /dev/null @@ -1,44 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] -#[cfg(target_os = "macos")] -fn main() { - use std::{env, path::PathBuf, process::Command}; - - let sdk_path = String::from_utf8( - Command::new("xcrun") - .args(["--sdk", "macosx", "--show-sdk-path"]) - .output() - .unwrap() - .stdout, - ) - .unwrap(); - let sdk_path = sdk_path.trim_end(); - - println!("cargo:rerun-if-changed=src/bindings.h"); - let bindings = bindgen::Builder::default() - .header("src/bindings.h") - .clang_arg(format!("-isysroot{}", sdk_path)) - .clang_arg("-xobjective-c") - .allowlist_type("CMItemIndex") - .allowlist_type("CMSampleTimingInfo") - .allowlist_type("CMVideoCodecType") - .allowlist_type("VTEncodeInfoFlags") - .allowlist_function("CMTimeMake") - .allowlist_var("kCVPixelFormatType_.*") - .allowlist_var("kCVReturn.*") - .allowlist_var("VTEncodeInfoFlags_.*") - .allowlist_var("kCMVideoCodecType_.*") - .allowlist_var("kCMTime.*") - .allowlist_var("kCMSampleAttachmentKey_.*") - .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) - .layout_tests(false) - .generate() - .expect("unable to generate bindings"); - - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - bindings - .write_to_file(out_path.join("bindings.rs")) - .expect("couldn't write dispatch bindings"); -} - -#[cfg(not(target_os = "macos"))] -fn main() {} diff --git a/crates/media/src/bindings.h b/crates/media/src/bindings.h deleted file mode 100644 index 4df283d0c1..0000000000 --- a/crates/media/src/bindings.h +++ /dev/null @@ -1,5 +0,0 @@ -#import -#import -#import -#import -#import diff --git a/crates/media/src/bindings.rs b/crates/media/src/bindings.rs deleted file mode 100644 index a1c78c17c4..0000000000 --- a/crates/media/src/bindings.rs +++ /dev/null @@ -1,10 +0,0 @@ -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] -#![allow(unused)] - -#[cfg(target_os = "macos")] -use objc::*; - -#[cfg(target_os = "macos")] -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/crates/media/src/media.rs b/crates/media/src/media.rs deleted file mode 100644 index c42bad62e7..0000000000 --- a/crates/media/src/media.rs +++ /dev/null @@ -1,352 +0,0 @@ -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] - -mod bindings; - -#[cfg(target_os = "macos")] -pub mod core_media { - #![allow(non_snake_case)] - - pub use crate::bindings::{ - CMItemIndex, CMSampleTimingInfo, CMTime, CMTimeMake, CMVideoCodecType, - kCMSampleAttachmentKey_NotSync, kCMTimeInvalid, kCMVideoCodecType_H264, - }; - use anyhow::Result; - use core_foundation::{ - array::{CFArray, CFArrayRef}, - base::{CFTypeID, OSStatus, TCFType}, - declare_TCFType, - dictionary::CFDictionary, - impl_CFTypeDescription, impl_TCFType, - string::CFString, - }; - use core_video::image_buffer::{CVImageBuffer, CVImageBufferRef}; - use std::{ffi::c_void, ptr}; - - #[repr(C)] - pub struct __CMSampleBuffer(c_void); - // The ref type must be a pointer to the underlying struct. - pub type CMSampleBufferRef = *const __CMSampleBuffer; - - declare_TCFType!(CMSampleBuffer, CMSampleBufferRef); - impl_TCFType!(CMSampleBuffer, CMSampleBufferRef, CMSampleBufferGetTypeID); - impl_CFTypeDescription!(CMSampleBuffer); - - impl CMSampleBuffer { - pub fn attachments(&self) -> Vec> { - unsafe { - let attachments = - CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true); - CFArray::::wrap_under_get_rule(attachments) - .into_iter() - .map(|attachments| { - CFDictionary::wrap_under_get_rule(attachments.as_concrete_TypeRef()) - }) - .collect() - } - } - - pub fn image_buffer(&self) -> Option { - unsafe { - let ptr = CMSampleBufferGetImageBuffer(self.as_concrete_TypeRef()); - if ptr.is_null() { - None - } else { - Some(CVImageBuffer::wrap_under_get_rule(ptr)) - } - } - } - - pub fn sample_timing_info(&self, index: usize) -> Result { - unsafe { - let mut timing_info = CMSampleTimingInfo { - duration: kCMTimeInvalid, - presentationTimeStamp: kCMTimeInvalid, - decodeTimeStamp: kCMTimeInvalid, - }; - let result = CMSampleBufferGetSampleTimingInfo( - self.as_concrete_TypeRef(), - index as CMItemIndex, - &mut timing_info, - ); - anyhow::ensure!( - result == 0, - "error getting sample timing info, code {result}" - ); - Ok(timing_info) - } - } - - pub fn format_description(&self) -> CMFormatDescription { - unsafe { - CMFormatDescription::wrap_under_get_rule(CMSampleBufferGetFormatDescription( - self.as_concrete_TypeRef(), - )) - } - } - - pub fn data(&self) -> CMBlockBuffer { - unsafe { - CMBlockBuffer::wrap_under_get_rule(CMSampleBufferGetDataBuffer( - self.as_concrete_TypeRef(), - )) - } - } - } - - #[link(name = "CoreMedia", kind = "framework")] - unsafe extern "C" { - fn CMSampleBufferGetTypeID() -> CFTypeID; - fn CMSampleBufferGetSampleAttachmentsArray( - buffer: CMSampleBufferRef, - create_if_necessary: bool, - ) -> CFArrayRef; - fn CMSampleBufferGetImageBuffer(buffer: CMSampleBufferRef) -> CVImageBufferRef; - fn CMSampleBufferGetSampleTimingInfo( - buffer: CMSampleBufferRef, - index: CMItemIndex, - timing_info_out: *mut CMSampleTimingInfo, - ) -> OSStatus; - fn CMSampleBufferGetFormatDescription(buffer: CMSampleBufferRef) -> CMFormatDescriptionRef; - fn CMSampleBufferGetDataBuffer(sample_buffer: CMSampleBufferRef) -> CMBlockBufferRef; - } - - #[repr(C)] - pub struct __CMFormatDescription(c_void); - pub type CMFormatDescriptionRef = *const __CMFormatDescription; - - declare_TCFType!(CMFormatDescription, CMFormatDescriptionRef); - impl_TCFType!( - CMFormatDescription, - CMFormatDescriptionRef, - CMFormatDescriptionGetTypeID - ); - impl_CFTypeDescription!(CMFormatDescription); - - impl CMFormatDescription { - pub fn h264_parameter_set_count(&self) -> usize { - unsafe { - let mut count = 0; - let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( - self.as_concrete_TypeRef(), - 0, - ptr::null_mut(), - ptr::null_mut(), - &mut count, - ptr::null_mut(), - ); - assert_eq!(result, 0); - count - } - } - - pub fn h264_parameter_set_at_index(&self, index: usize) -> Result<&[u8]> { - unsafe { - let mut bytes = ptr::null(); - let mut len = 0; - let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex( - self.as_concrete_TypeRef(), - index, - &mut bytes, - &mut len, - ptr::null_mut(), - ptr::null_mut(), - ); - anyhow::ensure!(result == 0, "error getting parameter set, code: {result}"); - Ok(std::slice::from_raw_parts(bytes, len)) - } - } - } - - #[link(name = "CoreMedia", kind = "framework")] - unsafe extern "C" { - fn CMFormatDescriptionGetTypeID() -> CFTypeID; - fn CMVideoFormatDescriptionGetH264ParameterSetAtIndex( - video_desc: CMFormatDescriptionRef, - parameter_set_index: usize, - parameter_set_pointer_out: *mut *const u8, - parameter_set_size_out: *mut usize, - parameter_set_count_out: *mut usize, - NALUnitHeaderLengthOut: *mut isize, - ) -> OSStatus; - } - - #[repr(C)] - pub struct __CMBlockBuffer(c_void); - pub type CMBlockBufferRef = *const __CMBlockBuffer; - - declare_TCFType!(CMBlockBuffer, CMBlockBufferRef); - impl_TCFType!(CMBlockBuffer, CMBlockBufferRef, CMBlockBufferGetTypeID); - impl_CFTypeDescription!(CMBlockBuffer); - - impl CMBlockBuffer { - pub fn bytes(&self) -> &[u8] { - unsafe { - let mut bytes = ptr::null(); - let mut len = 0; - let result = CMBlockBufferGetDataPointer( - self.as_concrete_TypeRef(), - 0, - &mut 0, - &mut len, - &mut bytes, - ); - assert!(result == 0, "could not get block buffer data"); - std::slice::from_raw_parts(bytes, len) - } - } - } - - #[link(name = "CoreMedia", kind = "framework")] - unsafe extern "C" { - fn CMBlockBufferGetTypeID() -> CFTypeID; - fn CMBlockBufferGetDataPointer( - buffer: CMBlockBufferRef, - offset: usize, - length_at_offset_out: *mut usize, - total_length_out: *mut usize, - data_pointer_out: *mut *const u8, - ) -> OSStatus; - } -} - -#[cfg(target_os = "macos")] -pub mod core_video { - #![allow(non_snake_case)] - - #[cfg(target_os = "macos")] - use core_foundation::{ - base::{CFTypeID, TCFType}, - declare_TCFType, impl_CFTypeDescription, impl_TCFType, - }; - #[cfg(target_os = "macos")] - use std::ffi::c_void; - - use crate::bindings::{CVReturn, kCVReturnSuccess}; - pub use crate::bindings::{ - kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, - kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar, - }; - use anyhow::Result; - use core_foundation::{ - base::kCFAllocatorDefault, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef, - }; - use foreign_types::ForeignTypeRef; - - use metal::{MTLDevice, MTLPixelFormat}; - use std::ptr; - - #[repr(C)] - pub struct __CVMetalTextureCache(c_void); - pub type CVMetalTextureCacheRef = *const __CVMetalTextureCache; - - declare_TCFType!(CVMetalTextureCache, CVMetalTextureCacheRef); - impl_TCFType!( - CVMetalTextureCache, - CVMetalTextureCacheRef, - CVMetalTextureCacheGetTypeID - ); - impl_CFTypeDescription!(CVMetalTextureCache); - - impl CVMetalTextureCache { - /// # Safety - /// - /// metal_device must be valid according to CVMetalTextureCacheCreate - pub unsafe fn new(metal_device: *mut MTLDevice) -> Result { - let mut this = ptr::null(); - let result = unsafe { - CVMetalTextureCacheCreate( - kCFAllocatorDefault, - ptr::null(), - metal_device, - ptr::null(), - &mut this, - ) - }; - anyhow::ensure!( - result == kCVReturnSuccess, - "could not create texture cache, code: {result}" - ); - unsafe { Ok(CVMetalTextureCache::wrap_under_create_rule(this)) } - } - - /// # Safety - /// - /// The arguments to this function must be valid according to CVMetalTextureCacheCreateTextureFromImage - pub unsafe fn create_texture_from_image( - &self, - source: ::core_video::image_buffer::CVImageBufferRef, - texture_attributes: CFDictionaryRef, - pixel_format: MTLPixelFormat, - width: usize, - height: usize, - plane_index: usize, - ) -> Result { - let mut this = ptr::null(); - let result = unsafe { - CVMetalTextureCacheCreateTextureFromImage( - kCFAllocatorDefault, - self.as_concrete_TypeRef(), - source, - texture_attributes, - pixel_format, - width, - height, - plane_index, - &mut this, - ) - }; - anyhow::ensure!( - result == kCVReturnSuccess, - "could not create texture, code: {result}" - ); - unsafe { Ok(CVMetalTexture::wrap_under_create_rule(this)) } - } - } - - #[link(name = "CoreVideo", kind = "framework")] - unsafe extern "C" { - fn CVMetalTextureCacheGetTypeID() -> CFTypeID; - fn CVMetalTextureCacheCreate( - allocator: CFAllocatorRef, - cache_attributes: CFDictionaryRef, - metal_device: *const MTLDevice, - texture_attributes: CFDictionaryRef, - cache_out: *mut CVMetalTextureCacheRef, - ) -> CVReturn; - fn CVMetalTextureCacheCreateTextureFromImage( - allocator: CFAllocatorRef, - texture_cache: CVMetalTextureCacheRef, - source_image: ::core_video::image_buffer::CVImageBufferRef, - texture_attributes: CFDictionaryRef, - pixel_format: MTLPixelFormat, - width: usize, - height: usize, - plane_index: usize, - texture_out: *mut CVMetalTextureRef, - ) -> CVReturn; - } - - #[repr(C)] - pub struct __CVMetalTexture(c_void); - pub type CVMetalTextureRef = *const __CVMetalTexture; - - declare_TCFType!(CVMetalTexture, CVMetalTextureRef); - impl_TCFType!(CVMetalTexture, CVMetalTextureRef, CVMetalTextureGetTypeID); - impl_CFTypeDescription!(CVMetalTexture); - - impl CVMetalTexture { - pub fn as_texture_ref(&self) -> &metal::TextureRef { - unsafe { - let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef()); - metal::TextureRef::from_ptr(texture as *mut _) - } - } - } - - #[link(name = "CoreVideo", kind = "framework")] - unsafe extern "C" { - fn CVMetalTextureGetTypeID() -> CFTypeID; - fn CVMetalTextureGetTexture(texture: CVMetalTextureRef) -> *mut c_void; - } -} diff --git a/crates/refineable/Cargo.toml b/crates/refineable/Cargo.toml deleted file mode 100644 index 4c4e02851b..0000000000 --- a/crates/refineable/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "refineable" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "A macro for creating 'refinement' types that can be used to partially initialize or mutate a complex struct" - -[lints] -workspace = true - -[lib] -path = "src/refineable.rs" -doctest = false - -[dependencies] -derive_refineable.workspace = true diff --git a/crates/refineable/LICENSE-APACHE b/crates/refineable/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/refineable/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/refineable/derive_refineable/Cargo.toml b/crates/refineable/derive_refineable/Cargo.toml deleted file mode 100644 index 75e9ef1f8f..0000000000 --- a/crates/refineable/derive_refineable/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "derive_refineable" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "A derive macro for creating refinement types in Rust" - - -[lints] -workspace = true - -[lib] -path = "src/derive_refineable.rs" -proc-macro = true -doctest = false - -[dependencies] -proc-macro2.workspace = true -quote.workspace = true -syn.workspace = true diff --git a/crates/refineable/derive_refineable/LICENSE-APACHE b/crates/refineable/derive_refineable/LICENSE-APACHE deleted file mode 120000 index 6e2b1da875..0000000000 --- a/crates/refineable/derive_refineable/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/refineable/derive_refineable/src/derive_refineable.rs b/crates/refineable/derive_refineable/src/derive_refineable.rs deleted file mode 100644 index ddf3855a4d..0000000000 --- a/crates/refineable/derive_refineable/src/derive_refineable.rs +++ /dev/null @@ -1,543 +0,0 @@ -use proc_macro::TokenStream; -use proc_macro2::TokenStream as TokenStream2; -use quote::{format_ident, quote}; -use syn::{ - DeriveInput, Field, FieldsNamed, PredicateType, TraitBound, Type, TypeParamBound, WhereClause, - WherePredicate, parse_macro_input, parse_quote, -}; - -#[proc_macro_derive(Refineable, attributes(refineable))] -pub fn derive_refineable(input: TokenStream) -> TokenStream { - let DeriveInput { - ident, - data, - generics, - attrs, - .. - } = parse_macro_input!(input); - - let refineable_attr = attrs.iter().find(|attr| attr.path().is_ident("refineable")); - - let mut impl_debug_on_refinement = false; - let mut derives_serialize = false; - let mut refinement_traits_to_derive = vec![]; - - if let Some(refineable_attr) = refineable_attr { - let _ = refineable_attr.parse_nested_meta(|meta| { - if meta.path.is_ident("Debug") { - impl_debug_on_refinement = true; - } else { - if meta.path.is_ident("Serialize") { - derives_serialize = true; - } - refinement_traits_to_derive.push(meta.path); - } - Ok(()) - }); - } - - let refinement_ident = format_ident!("{}Refinement", ident); - let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); - - let fields = match data { - syn::Data::Struct(syn::DataStruct { - fields: syn::Fields::Named(FieldsNamed { named, .. }), - .. - }) => named.into_iter().collect::>(), - _ => panic!("This derive macro only supports structs with named fields"), - }; - - let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect(); - let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect(); - let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect(); - - let field_attributes: Vec = fields - .iter() - .map(|f| { - if derives_serialize { - if is_refineable_field(f) { - quote! { #[serde(default, skip_serializing_if = "::refineable::IsEmpty::is_empty")] } - } else { - quote! { #[serde(skip_serializing_if = "::std::option::Option::is_none")] } - } - } else { - quote! {} - } - }) - .collect(); - - // Create trait bound that each wrapped type must implement Clone - let type_param_bounds: Vec<_> = wrapped_types - .iter() - .map(|ty| { - WherePredicate::Type(PredicateType { - lifetimes: None, - bounded_ty: ty.clone(), - colon_token: Default::default(), - bounds: { - let mut punctuated = syn::punctuated::Punctuated::new(); - punctuated.push_value(TypeParamBound::Trait(TraitBound { - paren_token: None, - modifier: syn::TraitBoundModifier::None, - lifetimes: None, - path: parse_quote!(Clone), - })); - - punctuated - }, - }) - }) - .collect(); - - // Append to where_clause or create a new one if it doesn't exist - let where_clause = match where_clause.cloned() { - Some(mut where_clause) => { - where_clause.predicates.extend(type_param_bounds); - where_clause.clone() - } - None => WhereClause { - where_token: Default::default(), - predicates: type_param_bounds.into_iter().collect(), - }, - }; - - let refineable_refine_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); - - if is_refineable { - quote! { - self.#name.refine(&refinement.#name); - } - } else if is_optional { - quote! { - if let Some(value) = &refinement.#name { - self.#name = Some(value.clone()); - } - } - } else { - quote! { - if let Some(value) = &refinement.#name { - self.#name = value.clone(); - } - } - } - }) - .collect(); - - let refineable_refined_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); - - if is_refineable { - quote! { - self.#name = self.#name.refined(refinement.#name); - } - } else if is_optional { - quote! { - if let Some(value) = refinement.#name { - self.#name = Some(value); - } - } - } else { - quote! { - if let Some(value) = refinement.#name { - self.#name = value; - } - } - } - }) - .collect(); - - let refinement_refine_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - - if is_refineable { - quote! { - self.#name.refine(&refinement.#name); - } - } else { - quote! { - if let Some(value) = &refinement.#name { - self.#name = Some(value.clone()); - } - } - } - }) - .collect(); - - let refinement_refined_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - - if is_refineable { - quote! { - self.#name = self.#name.refined(refinement.#name); - } - } else { - quote! { - if let Some(value) = refinement.#name { - self.#name = Some(value); - } - } - } - }) - .collect(); - - let from_refinement_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); - - if is_refineable { - quote! { - #name: value.#name.into(), - } - } else if is_optional { - quote! { - #name: value.#name.map(|v| v.into()), - } - } else { - quote! { - #name: value.#name.map(|v| v.into()).unwrap_or_default(), - } - } - }) - .collect(); - - let debug_impl = if impl_debug_on_refinement { - let refinement_field_debugs: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - quote! { - if self.#name.is_some() { - debug_struct.field(stringify!(#name), &self.#name); - } else { - all_some = false; - } - } - }) - .collect(); - - quote! { - impl #impl_generics std::fmt::Debug for #refinement_ident #ty_generics - #where_clause - { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut debug_struct = f.debug_struct(stringify!(#refinement_ident)); - let mut all_some = true; - #( #refinement_field_debugs )* - if all_some { - debug_struct.finish() - } else { - debug_struct.finish_non_exhaustive() - } - } - } - } - } else { - quote! {} - }; - - let refinement_is_empty_conditions: Vec = fields - .iter() - .enumerate() - .map(|(i, field)| { - let name = &field.ident; - - let condition = if is_refineable_field(field) { - quote! { self.#name.is_empty() } - } else { - quote! { self.#name.is_none() } - }; - - if i < fields.len() - 1 { - quote! { #condition && } - } else { - condition - } - }) - .collect(); - - let refineable_is_superset_conditions: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); - - if is_refineable { - quote! { - if !self.#name.is_superset_of(&refinement.#name) { - return false; - } - } - } else if is_optional { - quote! { - if refinement.#name.is_some() && &self.#name != &refinement.#name { - return false; - } - } - } else { - quote! { - if let Some(refinement_value) = &refinement.#name { - if &self.#name != refinement_value { - return false; - } - } - } - } - }) - .collect(); - - let refinement_is_superset_conditions: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - - if is_refineable { - quote! { - if !self.#name.is_superset_of(&refinement.#name) { - return false; - } - } - } else { - quote! { - if refinement.#name.is_some() && &self.#name != &refinement.#name { - return false; - } - } - } - }) - .collect(); - - let refineable_subtract_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - let is_optional = is_optional_field(field); - - if is_refineable { - quote! { - #name: self.#name.subtract(&refinement.#name), - } - } else if is_optional { - quote! { - #name: if &self.#name == &refinement.#name { - None - } else { - self.#name.clone() - }, - } - } else { - quote! { - #name: if let Some(refinement_value) = &refinement.#name { - if &self.#name == refinement_value { - None - } else { - Some(self.#name.clone()) - } - } else { - Some(self.#name.clone()) - }, - } - } - }) - .collect(); - - let refinement_subtract_assignments: Vec = fields - .iter() - .map(|field| { - let name = &field.ident; - let is_refineable = is_refineable_field(field); - - if is_refineable { - quote! { - #name: self.#name.subtract(&refinement.#name), - } - } else { - quote! { - #name: if &self.#name == &refinement.#name { - None - } else { - self.#name.clone() - }, - } - } - }) - .collect(); - - let mut derive_stream = quote! {}; - for trait_to_derive in refinement_traits_to_derive { - derive_stream.extend(quote! { #[derive(#trait_to_derive)] }) - } - - let r#gen = quote! { - /// A refinable version of [`#ident`], see that documentation for details. - #[derive(Clone)] - #derive_stream - pub struct #refinement_ident #impl_generics { - #( - #[allow(missing_docs)] - #field_attributes - #field_visibilities #field_names: #wrapped_types - ),* - } - - impl #impl_generics Refineable for #ident #ty_generics - #where_clause - { - type Refinement = #refinement_ident #ty_generics; - - fn refine(&mut self, refinement: &Self::Refinement) { - #( #refineable_refine_assignments )* - } - - fn refined(mut self, refinement: Self::Refinement) -> Self { - #( #refineable_refined_assignments )* - self - } - - fn is_superset_of(&self, refinement: &Self::Refinement) -> bool - { - #( #refineable_is_superset_conditions )* - true - } - - fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement - { - #refinement_ident { - #( #refineable_subtract_assignments )* - } - } - } - - impl #impl_generics Refineable for #refinement_ident #ty_generics - #where_clause - { - type Refinement = #refinement_ident #ty_generics; - - fn refine(&mut self, refinement: &Self::Refinement) { - #( #refinement_refine_assignments )* - } - - fn refined(mut self, refinement: Self::Refinement) -> Self { - #( #refinement_refined_assignments )* - self - } - - fn is_superset_of(&self, refinement: &Self::Refinement) -> bool - { - #( #refinement_is_superset_conditions )* - true - } - - fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement - { - #refinement_ident { - #( #refinement_subtract_assignments )* - } - } - } - - impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics - #where_clause - { - fn is_empty(&self) -> bool { - #( #refinement_is_empty_conditions )* - } - } - - impl #impl_generics From<#refinement_ident #ty_generics> for #ident #ty_generics - #where_clause - { - fn from(value: #refinement_ident #ty_generics) -> Self { - Self { - #( #from_refinement_assignments )* - } - } - } - - impl #impl_generics ::core::default::Default for #refinement_ident #ty_generics - #where_clause - { - fn default() -> Self { - #refinement_ident { - #( #field_names: Default::default() ),* - } - } - } - - impl #impl_generics #refinement_ident #ty_generics - #where_clause - { - /// Returns `true` if all fields are `Some` - pub fn is_some(&self) -> bool { - #( - if self.#field_names.is_some() { - return true; - } - )* - false - } - } - - #debug_impl - }; - r#gen.into() -} - -fn is_refineable_field(f: &Field) -> bool { - f.attrs - .iter() - .any(|attr| attr.path().is_ident("refineable")) -} - -fn is_optional_field(f: &Field) -> bool { - if let Type::Path(typepath) = &f.ty - && typepath.qself.is_none() - { - let segments = &typepath.path.segments; - if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") { - return true; - } - } - false -} - -fn get_wrapper_type(field: &Field, ty: &Type) -> syn::Type { - if is_refineable_field(field) { - let struct_name = if let Type::Path(tp) = ty { - tp.path.segments.last().unwrap().ident.clone() - } else { - panic!("Expected struct type for a refineable field"); - }; - let refinement_struct_name = format_ident!("{}Refinement", struct_name); - let generics = if let Type::Path(tp) = ty { - &tp.path.segments.last().unwrap().arguments - } else { - &syn::PathArguments::None - }; - parse_quote!(#refinement_struct_name #generics) - } else if is_optional_field(field) { - ty.clone() - } else { - parse_quote!(Option<#ty>) - } -} diff --git a/crates/refineable/src/refineable.rs b/crates/refineable/src/refineable.rs deleted file mode 100644 index d2a7c3d3f2..0000000000 --- a/crates/refineable/src/refineable.rs +++ /dev/null @@ -1,132 +0,0 @@ -pub use derive_refineable::Refineable; - -/// A trait for types that can be refined with partial updates. -/// -/// The `Refineable` trait enables hierarchical configuration patterns where a base configuration -/// can be selectively overridden by refinements. This is particularly useful for styling and -/// settings, and theme hierarchies. -/// -/// # Derive Macro -/// -/// The `#[derive(Refineable)]` macro automatically generates a companion refinement type and -/// implements this trait. For a struct `Style`, it creates `StyleRefinement` where each field is -/// wrapped appropriately: -/// -/// - **Refineable fields** (marked with `#[refineable]`): Become the corresponding refinement type -/// (e.g., `Bar` becomes `BarRefinement`) -/// - **Optional fields** (`Option`): Remain as `Option` -/// - **Regular fields**: Become `Option` -/// -/// ## Attributes -/// -/// The derive macro supports these attributes on the struct: -/// - `#[refineable(Debug)]`: Implements `Debug` for the refinement type -/// - `#[refineable(Serialize)]`: Derives `Serialize` which skips serializing `None` -/// - `#[refineable(OtherTrait)]`: Derives additional traits on the refinement type -/// -/// Fields can be marked with: -/// - `#[refineable]`: Field is itself refineable (uses nested refinement type) -pub trait Refineable: Clone { - type Refinement: Refineable + IsEmpty + Default; - - /// Applies the given refinement to this instance, modifying it in place. - /// - /// Only non-empty values in the refinement are applied. - /// - /// * For refineable fields, this recursively calls `refine`. - /// * For other fields, the value is replaced if present in the refinement. - fn refine(&mut self, refinement: &Self::Refinement); - - /// Returns a new instance with the refinement applied, equivalent to cloning `self` and calling - /// `refine` on it. - fn refined(self, refinement: Self::Refinement) -> Self; - - /// Creates an instance from a cascade by merging all refinements atop the default value. - fn from_cascade(cascade: &Cascade) -> Self - where - Self: Default + Sized, - { - Self::default().refined(cascade.merged()) - } - - /// Returns `true` if this instance would contain all values from the refinement. - /// - /// For refineable fields, this recursively checks `is_superset_of`. For other fields, this - /// checks if the refinement's `Some` values match this instance's values. - fn is_superset_of(&self, refinement: &Self::Refinement) -> bool; - - /// Returns a refinement that represents the difference between this instance and the given - /// refinement. - /// - /// For refineable fields, this recursively calls `subtract`. For other fields, the field is - /// `None` if the field's value is equal to the refinement. - fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement; -} - -pub trait IsEmpty { - /// Returns `true` if applying this refinement would have no effect. - fn is_empty(&self) -> bool; -} - -/// A cascade of refinements that can be merged in priority order. -/// -/// A cascade maintains a sequence of optional refinements where later entries -/// take precedence over earlier ones. The first slot (index 0) is always the -/// base refinement and is guaranteed to be present. -/// -/// This is useful for implementing configuration hierarchies like CSS cascading, -/// where styles from different sources (user agent, user, author) are combined -/// with specific precedence rules. -pub struct Cascade(Vec>); - -impl Default for Cascade { - fn default() -> Self { - Self(vec![Some(Default::default())]) - } -} - -/// A handle to a specific slot in a cascade. -/// -/// Slots are used to identify specific positions in the cascade where -/// refinements can be set or updated. -#[derive(Copy, Clone)] -pub struct CascadeSlot(usize); - -impl Cascade { - /// Reserves a new slot in the cascade and returns a handle to it. - /// - /// The new slot is initially empty (`None`) and can be populated later - /// using `set()`. - pub fn reserve(&mut self) -> CascadeSlot { - self.0.push(None); - CascadeSlot(self.0.len() - 1) - } - - /// Returns a mutable reference to the base refinement (slot 0). - /// - /// The base refinement is always present and serves as the foundation - /// for the cascade. - pub fn base(&mut self) -> &mut S::Refinement { - self.0[0].as_mut().unwrap() - } - - /// Sets the refinement for a specific slot in the cascade. - /// - /// Setting a slot to `None` effectively removes it from consideration - /// during merging. - pub fn set(&mut self, slot: CascadeSlot, refinement: Option) { - self.0[slot.0] = refinement - } - - /// Merges all refinements in the cascade into a single refinement. - /// - /// Refinements are applied in order, with later slots taking precedence. - /// Empty slots (`None`) are skipped during merging. - pub fn merged(&self) -> S::Refinement { - let mut merged = self.0[0].clone().unwrap(); - for refinement in self.0.iter().skip(1).flatten() { - merged.refine(refinement); - } - merged - } -} diff --git a/crates/reqwest_client/Cargo.toml b/crates/reqwest_client/Cargo.toml deleted file mode 100644 index 7fd50237d9..0000000000 --- a/crates/reqwest_client/Cargo.toml +++ /dev/null @@ -1,31 +0,0 @@ -[package] -name = "reqwest_client" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[features] -test-support = [] - -[lib] -path = "src/reqwest_client.rs" -doctest = true - -[dependencies] -anyhow.workspace = true -bytes.workspace = true -futures.workspace = true -http_client.workspace = true -http_client_tls.workspace = true -serde.workspace = true -log.workspace = true -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } -regex.workspace = true -reqwest.workspace = true - -[dev-dependencies] -gpui.workspace = true diff --git a/crates/reqwest_client/LICENSE-APACHE b/crates/reqwest_client/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/reqwest_client/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/reqwest_client/src/reqwest_client.rs b/crates/reqwest_client/src/reqwest_client.rs deleted file mode 100644 index 8a1ee45e1c..0000000000 --- a/crates/reqwest_client/src/reqwest_client.rs +++ /dev/null @@ -1,320 +0,0 @@ -use std::error::Error; -use std::sync::{LazyLock, OnceLock}; -use std::{borrow::Cow, mem, pin::Pin, task::Poll, time::Duration}; - -use anyhow::anyhow; -use bytes::{BufMut, Bytes, BytesMut}; -use futures::{AsyncRead, FutureExt as _, TryStreamExt as _}; -use http_client::{RedirectPolicy, Url, http}; -use regex::Regex; -use reqwest::{ - header::{HeaderMap, HeaderValue}, - redirect, -}; - -const DEFAULT_CAPACITY: usize = 4096; -static RUNTIME: OnceLock = OnceLock::new(); -static REDACT_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"key=[^&]+").unwrap()); - -pub struct ReqwestClient { - client: reqwest::Client, - proxy: Option, - user_agent: Option, - handle: tokio::runtime::Handle, -} - -impl ReqwestClient { - fn builder() -> reqwest::ClientBuilder { - reqwest::Client::builder() - .use_rustls_tls() - .connect_timeout(Duration::from_secs(10)) - } - - pub fn new() -> Self { - Self::builder() - .build() - .expect("Failed to initialize HTTP client") - .into() - } - - pub fn user_agent(agent: &str) -> anyhow::Result { - let mut map = HeaderMap::new(); - map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?); - let client = Self::builder().default_headers(map).build()?; - Ok(client.into()) - } - - pub fn proxy_and_user_agent(proxy: Option, user_agent: &str) -> anyhow::Result { - let user_agent = HeaderValue::from_str(user_agent)?; - - let mut map = HeaderMap::new(); - map.insert(http::header::USER_AGENT, user_agent.clone()); - let mut client = Self::builder().default_headers(map); - let client_has_proxy; - - if let Some(proxy) = proxy.as_ref().and_then(|proxy_url| { - reqwest::Proxy::all(proxy_url.clone()) - .inspect_err(|e| { - log::error!( - "Failed to parse proxy URL '{}': {}", - proxy_url, - e.source().unwrap_or(&e as &_) - ) - }) - .ok() - }) { - // Respect NO_PROXY env var - client = client.proxy(proxy.no_proxy(reqwest::NoProxy::from_env())); - client_has_proxy = true; - } else { - client_has_proxy = false; - }; - - let client = client - .use_preconfigured_tls(http_client_tls::tls_config()) - .build()?; - let mut client: ReqwestClient = client.into(); - client.proxy = client_has_proxy.then_some(proxy).flatten(); - client.user_agent = Some(user_agent); - Ok(client) - } -} - -pub fn runtime() -> &'static tokio::runtime::Runtime { - RUNTIME.get_or_init(|| { - tokio::runtime::Builder::new_multi_thread() - // Since we now have two executors, let's try to keep our footprint small - .worker_threads(1) - .enable_all() - .build() - .expect("Failed to initialize HTTP client") - }) -} - -impl From for ReqwestClient { - fn from(client: reqwest::Client) -> Self { - let handle = tokio::runtime::Handle::try_current().unwrap_or_else(|_| { - log::debug!("no tokio runtime found, creating one for Reqwest..."); - runtime().handle().clone() - }); - Self { - client, - handle, - proxy: None, - user_agent: None, - } - } -} - -// This struct is essentially a re-implementation of -// https://docs.rs/tokio-util/0.7.12/tokio_util/io/struct.ReaderStream.html -// except outside of Tokio's aegis -struct StreamReader { - reader: Option>>, - buf: BytesMut, - capacity: usize, -} - -impl StreamReader { - fn new(reader: Pin>) -> Self { - Self { - reader: Some(reader), - buf: BytesMut::new(), - capacity: DEFAULT_CAPACITY, - } - } -} - -impl futures::Stream for StreamReader { - type Item = std::io::Result; - - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> Poll> { - let mut this = self.as_mut(); - - let mut reader = match this.reader.take() { - Some(r) => r, - None => return Poll::Ready(None), - }; - - if this.buf.capacity() == 0 { - let capacity = this.capacity; - this.buf.reserve(capacity); - } - - match poll_read_buf(&mut reader, cx, &mut this.buf) { - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => { - self.reader = None; - - Poll::Ready(Some(Err(err))) - } - Poll::Ready(Ok(0)) => { - self.reader = None; - Poll::Ready(None) - } - Poll::Ready(Ok(_)) => { - let chunk = this.buf.split(); - self.reader = Some(reader); - Poll::Ready(Some(Ok(chunk.freeze()))) - } - } - } -} - -/// Implementation from -/// Specialized for this use case -pub fn poll_read_buf( - io: &mut Pin>, - cx: &mut std::task::Context<'_>, - buf: &mut BytesMut, -) -> Poll> { - if !buf.has_remaining_mut() { - return Poll::Ready(Ok(0)); - } - - let n = { - let dst = buf.chunk_mut(); - - // Safety: `chunk_mut()` returns a `&mut UninitSlice`, and `UninitSlice` is a - // transparent wrapper around `[MaybeUninit]`. - let dst = unsafe { &mut *(dst as *mut _ as *mut [std::mem::MaybeUninit]) }; - let mut buf = tokio::io::ReadBuf::uninit(dst); - let ptr = buf.filled().as_ptr(); - let unfilled_portion = buf.initialize_unfilled(); - // SAFETY: Pin projection - let io_pin = unsafe { Pin::new_unchecked(io) }; - std::task::ready!(io_pin.poll_read(cx, unfilled_portion)?); - - // Ensure the pointer does not change from under us - assert_eq!(ptr, buf.filled().as_ptr()); - buf.filled().len() - }; - - // Safety: This is guaranteed to be the number of initialized (and read) - // bytes due to the invariants provided by `ReadBuf::filled`. - unsafe { - buf.advance_mut(n); - } - - Poll::Ready(Ok(n)) -} - -fn redact_error(mut error: reqwest::Error) -> reqwest::Error { - if let Some(url) = error.url_mut() - && let Some(query) = url.query() - && let Cow::Owned(redacted) = REDACT_REGEX.replace_all(query, "key=REDACTED") - { - url.set_query(Some(redacted.as_str())); - } - error -} - -impl http_client::HttpClient for ReqwestClient { - fn proxy(&self) -> Option<&Url> { - self.proxy.as_ref() - } - - fn user_agent(&self) -> Option<&HeaderValue> { - self.user_agent.as_ref() - } - - fn send( - &self, - req: http::Request, - ) -> futures::future::BoxFuture< - 'static, - anyhow::Result>, - > { - let (parts, body) = req.into_parts(); - - let mut request = self.client.request(parts.method, parts.uri.to_string()); - request = request.headers(parts.headers); - if let Some(redirect_policy) = parts.extensions.get::() { - request = request.redirect_policy(match redirect_policy { - RedirectPolicy::NoFollow => redirect::Policy::none(), - RedirectPolicy::FollowLimit(limit) => redirect::Policy::limited(*limit as usize), - RedirectPolicy::FollowAll => redirect::Policy::limited(100), - }); - } - let request = request.body(match body.0 { - http_client::Inner::Empty => reqwest::Body::default(), - http_client::Inner::Bytes(cursor) => cursor.into_inner().into(), - http_client::Inner::AsyncReader(stream) => { - reqwest::Body::wrap_stream(StreamReader::new(stream)) - } - }); - - let handle = self.handle.clone(); - async move { - let mut response = handle - .spawn(async { request.send().await }) - .await? - .map_err(redact_error)?; - - let headers = mem::take(response.headers_mut()); - let mut builder = http::Response::builder() - .status(response.status().as_u16()) - .version(response.version()); - *builder.headers_mut().unwrap() = headers; - - let bytes = response - .bytes_stream() - .map_err(futures::io::Error::other) - .into_async_read(); - let body = http_client::AsyncBody::from_reader(bytes); - - builder.body(body).map_err(|e| anyhow!(e)) - } - .boxed() - } -} - -#[cfg(test)] -mod tests { - use http_client::{HttpClient, Url}; - - use crate::ReqwestClient; - - #[test] - fn test_proxy_uri() { - let client = ReqwestClient::new(); - assert_eq!(client.proxy(), None); - - let proxy = Url::parse("http://localhost:10809").unwrap(); - let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap(); - assert_eq!(client.proxy(), Some(&proxy)); - - let proxy = Url::parse("https://localhost:10809").unwrap(); - let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap(); - assert_eq!(client.proxy(), Some(&proxy)); - - let proxy = Url::parse("socks4://localhost:10808").unwrap(); - let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap(); - assert_eq!(client.proxy(), Some(&proxy)); - - let proxy = Url::parse("socks4a://localhost:10808").unwrap(); - let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap(); - assert_eq!(client.proxy(), Some(&proxy)); - - let proxy = Url::parse("socks5://localhost:10808").unwrap(); - let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap(); - assert_eq!(client.proxy(), Some(&proxy)); - - let proxy = Url::parse("socks5h://localhost:10808").unwrap(); - let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap(); - assert_eq!(client.proxy(), Some(&proxy)); - } - - #[test] - fn test_invalid_proxy_uri() { - let proxy = Url::parse("socks://127.0.0.1:20170").unwrap(); - let client = ReqwestClient::proxy_and_user_agent(Some(proxy), "test").unwrap(); - assert!( - client.proxy.is_none(), - "An invalid proxy URL should add no proxy to the client!" - ) - } -} diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml deleted file mode 100644 index bbab41dcdb..0000000000 --- a/crates/scheduler/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "scheduler" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/scheduler.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -async-task.workspace = true -backtrace.workspace = true -chrono.workspace = true -futures.workspace = true -parking_lot.workspace = true -rand.workspace = true diff --git a/crates/scheduler/LICENSE-APACHE b/crates/scheduler/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/scheduler/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/scheduler/src/clock.rs b/crates/scheduler/src/clock.rs deleted file mode 100644 index 017643c4eb..0000000000 --- a/crates/scheduler/src/clock.rs +++ /dev/null @@ -1,47 +0,0 @@ -use chrono::{DateTime, Utc}; -use parking_lot::Mutex; -use std::time::{Duration, Instant}; - -pub trait Clock { - fn utc_now(&self) -> DateTime; - fn now(&self) -> Instant; -} - -pub struct TestClock(Mutex); - -struct TestClockState { - now: Instant, - utc_now: DateTime, -} - -impl TestClock { - pub fn new() -> Self { - const START_TIME: &str = "2025-07-01T23:59:58-00:00"; - let utc_now = DateTime::parse_from_rfc3339(START_TIME).unwrap().to_utc(); - Self(Mutex::new(TestClockState { - now: Instant::now(), - utc_now, - })) - } - - pub fn set_utc_now(&self, now: DateTime) { - let mut state = self.0.lock(); - state.utc_now = now; - } - - pub fn advance(&self, duration: Duration) { - let mut state = self.0.lock(); - state.now += duration; - state.utc_now += duration; - } -} - -impl Clock for TestClock { - fn utc_now(&self) -> DateTime { - self.0.lock().utc_now - } - - fn now(&self) -> Instant { - self.0.lock().now - } -} diff --git a/crates/scheduler/src/executor.rs b/crates/scheduler/src/executor.rs deleted file mode 100644 index a5a0126860..0000000000 --- a/crates/scheduler/src/executor.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::{Scheduler, SessionId, Timer}; -use futures::FutureExt as _; -use std::{ - future::Future, - marker::PhantomData, - mem::ManuallyDrop, - panic::Location, - pin::Pin, - rc::Rc, - sync::Arc, - task::{Context, Poll}, - thread::{self, ThreadId}, - time::Duration, -}; - -#[derive(Clone)] -pub struct ForegroundExecutor { - session_id: SessionId, - scheduler: Arc, - not_send: PhantomData>, -} - -impl ForegroundExecutor { - pub fn new(session_id: SessionId, scheduler: Arc) -> Self { - Self { - session_id, - scheduler, - not_send: PhantomData, - } - } - - #[track_caller] - pub fn spawn(&self, future: F) -> Task - where - F: Future + 'static, - F::Output: 'static, - { - let session_id = self.session_id; - let scheduler = Arc::clone(&self.scheduler); - let (runnable, task) = spawn_local_with_source_location(future, move |runnable| { - scheduler.schedule_foreground(session_id, runnable); - }); - runnable.schedule(); - Task(TaskState::Spawned(task)) - } - - pub fn block_on(&self, future: Fut) -> Fut::Output { - let mut output = None; - self.scheduler.block( - Some(self.session_id), - async { output = Some(future.await) }.boxed_local(), - None, - ); - output.unwrap() - } - - pub fn block_with_timeout( - &self, - timeout: Duration, - mut future: Fut, - ) -> Result { - let mut output = None; - self.scheduler.block( - Some(self.session_id), - async { output = Some((&mut future).await) }.boxed_local(), - Some(timeout), - ); - output.ok_or(future) - } - - pub fn timer(&self, duration: Duration) -> Timer { - self.scheduler.timer(duration) - } -} - -#[derive(Clone)] -pub struct BackgroundExecutor { - scheduler: Arc, -} - -impl BackgroundExecutor { - pub fn new(scheduler: Arc) -> Self { - Self { scheduler } - } - - pub fn spawn(&self, future: F) -> Task - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - let scheduler = Arc::clone(&self.scheduler); - let (runnable, task) = async_task::spawn(future, move |runnable| { - scheduler.schedule_background(runnable); - }); - runnable.schedule(); - Task(TaskState::Spawned(task)) - } - - pub fn timer(&self, duration: Duration) -> Timer { - self.scheduler.timer(duration) - } - - pub fn scheduler(&self) -> &Arc { - &self.scheduler - } -} - -/// Task is a primitive that allows work to happen in the background. -/// -/// It implements [`Future`] so you can `.await` on it. -/// -/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows -/// the task to continue running, but with no way to return a value. -#[must_use] -#[derive(Debug)] -pub struct Task(TaskState); - -#[derive(Debug)] -enum TaskState { - /// A task that is ready to return a value - Ready(Option), - - /// A task that is currently running. - Spawned(async_task::Task), -} - -impl Task { - /// Creates a new task that will resolve with the value - pub fn ready(val: T) -> Self { - Task(TaskState::Ready(Some(val))) - } - - pub fn is_ready(&self) -> bool { - match &self.0 { - TaskState::Ready(_) => true, - TaskState::Spawned(task) => task.is_finished(), - } - } - - /// Detaching a task runs it to completion in the background - pub fn detach(self) { - match self { - Task(TaskState::Ready(_)) => {} - Task(TaskState::Spawned(task)) => task.detach(), - } - } -} - -impl Future for Task { - type Output = T; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - match unsafe { self.get_unchecked_mut() } { - Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()), - Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx), - } - } -} - -/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics. -/// -/// Copy-modified from: -/// -#[track_caller] -fn spawn_local_with_source_location( - future: Fut, - schedule: S, -) -> (async_task::Runnable, async_task::Task) -where - Fut: Future + 'static, - Fut::Output: 'static, - S: async_task::Schedule + Send + Sync + 'static, -{ - #[inline] - fn thread_id() -> ThreadId { - std::thread_local! { - static ID: ThreadId = thread::current().id(); - } - ID.try_with(|id| *id) - .unwrap_or_else(|_| thread::current().id()) - } - - struct Checked { - id: ThreadId, - inner: ManuallyDrop, - location: &'static Location<'static>, - } - - impl Drop for Checked { - fn drop(&mut self) { - assert!( - self.id == thread_id(), - "local task dropped by a thread that didn't spawn it. Task spawned at {}", - self.location - ); - unsafe { - ManuallyDrop::drop(&mut self.inner); - } - } - } - - impl Future for Checked { - type Output = F::Output; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - assert!( - self.id == thread_id(), - "local task polled by a thread that didn't spawn it. Task spawned at {}", - self.location - ); - unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) } - } - } - - // Wrap the future into one that checks which thread it's on. - let future = Checked { - id: thread_id(), - inner: ManuallyDrop::new(future), - location: Location::caller(), - }; - - unsafe { async_task::spawn_unchecked(future, schedule) } -} diff --git a/crates/scheduler/src/scheduler.rs b/crates/scheduler/src/scheduler.rs deleted file mode 100644 index 8d485e009c..0000000000 --- a/crates/scheduler/src/scheduler.rs +++ /dev/null @@ -1,63 +0,0 @@ -mod clock; -mod executor; -mod test_scheduler; -#[cfg(test)] -mod tests; - -pub use clock::*; -pub use executor::*; -pub use test_scheduler::*; - -use async_task::Runnable; -use futures::{FutureExt as _, channel::oneshot, future::LocalBoxFuture}; -use std::{ - future::Future, - pin::Pin, - sync::Arc, - task::{Context, Poll}, - time::Duration, -}; - -pub trait Scheduler: Send + Sync { - fn block( - &self, - session_id: Option, - future: LocalBoxFuture<()>, - timeout: Option, - ); - fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable); - fn schedule_background(&self, runnable: Runnable); - fn timer(&self, timeout: Duration) -> Timer; - fn clock(&self) -> Arc; - fn as_test(&self) -> &TestScheduler { - panic!("this is not a test scheduler") - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] -pub struct SessionId(u16); - -impl SessionId { - pub fn new(id: u16) -> Self { - SessionId(id) - } -} - -pub struct Timer(oneshot::Receiver<()>); - -impl Timer { - pub fn new(rx: oneshot::Receiver<()>) -> Self { - Timer(rx) - } -} - -impl Future for Timer { - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> { - match self.0.poll_unpin(cx) { - Poll::Ready(_) => Poll::Ready(()), - Poll::Pending => Poll::Pending, - } - } -} diff --git a/crates/scheduler/src/test_scheduler.rs b/crates/scheduler/src/test_scheduler.rs deleted file mode 100644 index d56db6b839..0000000000 --- a/crates/scheduler/src/test_scheduler.rs +++ /dev/null @@ -1,541 +0,0 @@ -use crate::{ - BackgroundExecutor, Clock, ForegroundExecutor, Scheduler, SessionId, TestClock, Timer, -}; -use async_task::Runnable; -use backtrace::{Backtrace, BacktraceFrame}; -use futures::{FutureExt as _, channel::oneshot, future::LocalBoxFuture}; -use parking_lot::Mutex; -use rand::prelude::*; -use std::{ - any::type_name_of_val, - collections::{BTreeMap, VecDeque}, - env, - fmt::Write, - future::Future, - mem, - ops::RangeInclusive, - panic::{self, AssertUnwindSafe}, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, - }, - task::{Context, Poll, RawWaker, RawWakerVTable, Waker}, - thread::{self, Thread}, - time::{Duration, Instant}, -}; - -const PENDING_TRACES_VAR_NAME: &str = "PENDING_TRACES"; - -pub struct TestScheduler { - clock: Arc, - rng: Arc>, - state: Arc>, - thread: Thread, -} - -impl TestScheduler { - /// Run a test once with default configuration (seed 0) - pub fn once(f: impl AsyncFnOnce(Arc) -> R) -> R { - Self::with_seed(0, f) - } - - /// Run a test multiple times with sequential seeds (0, 1, 2, ...) - pub fn many( - default_iterations: usize, - mut f: impl AsyncFnMut(Arc) -> R, - ) -> Vec { - let num_iterations = std::env::var("ITERATIONS") - .map(|iterations| iterations.parse().unwrap()) - .unwrap_or(default_iterations); - - let seed = std::env::var("SEED") - .map(|seed| seed.parse().unwrap()) - .unwrap_or(0); - - (seed..num_iterations as u64) - .map(|seed| { - let mut unwind_safe_f = AssertUnwindSafe(&mut f); - eprintln!("Running seed: {seed}"); - match panic::catch_unwind(move || Self::with_seed(seed, &mut *unwind_safe_f)) { - Ok(result) => result, - Err(error) => { - eprintln!("\x1b[31mFailing Seed: {seed}\x1b[0m"); - panic::resume_unwind(error); - } - } - }) - .collect() - } - - fn with_seed(seed: u64, f: impl AsyncFnOnce(Arc) -> R) -> R { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(seed))); - let future = f(scheduler.clone()); - let result = scheduler.foreground().block_on(future); - scheduler.run(); // Ensure spawned tasks finish up before returning in tests - result - } - - pub fn new(config: TestSchedulerConfig) -> Self { - Self { - rng: Arc::new(Mutex::new(StdRng::seed_from_u64(config.seed))), - state: Arc::new(Mutex::new(SchedulerState { - runnables: VecDeque::new(), - timers: Vec::new(), - blocked_sessions: Vec::new(), - randomize_order: config.randomize_order, - allow_parking: config.allow_parking, - timeout_ticks: config.timeout_ticks, - next_session_id: SessionId(0), - capture_pending_traces: config.capture_pending_traces, - pending_traces: BTreeMap::new(), - next_trace_id: TraceId(0), - })), - clock: Arc::new(TestClock::new()), - thread: thread::current(), - } - } - - pub fn clock(&self) -> Arc { - self.clock.clone() - } - - pub fn rng(&self) -> Arc> { - self.rng.clone() - } - - pub fn set_timeout_ticks(&self, timeout_ticks: RangeInclusive) { - self.state.lock().timeout_ticks = timeout_ticks; - } - - pub fn allow_parking(&self) { - self.state.lock().allow_parking = true; - } - - pub fn forbid_parking(&self) { - self.state.lock().allow_parking = false; - } - - /// Create a foreground executor for this scheduler - pub fn foreground(self: &Arc) -> ForegroundExecutor { - let session_id = { - let mut state = self.state.lock(); - state.next_session_id.0 += 1; - state.next_session_id - }; - ForegroundExecutor::new(session_id, self.clone()) - } - - /// Create a background executor for this scheduler - pub fn background(self: &Arc) -> BackgroundExecutor { - BackgroundExecutor::new(self.clone()) - } - - pub fn yield_random(&self) -> Yield { - let rng = &mut *self.rng.lock(); - if rng.random_bool(0.1) { - Yield(rng.random_range(10..20)) - } else { - Yield(rng.random_range(0..2)) - } - } - - pub fn run(&self) { - while self.step() { - // Continue until no work remains - } - } - - pub fn run_with_clock_advancement(&self) { - while self.step() || self.advance_clock_to_next_timer() { - // Continue until no work remains - } - } - - fn step(&self) -> bool { - let elapsed_timers = { - let mut state = self.state.lock(); - let end_ix = state - .timers - .partition_point(|timer| timer.expiration <= self.clock.now()); - state.timers.drain(..end_ix).collect::>() - }; - - if !elapsed_timers.is_empty() { - return true; - } - - let runnable = { - let state = &mut *self.state.lock(); - let ix = state.runnables.iter().position(|runnable| { - runnable - .session_id - .is_none_or(|session_id| !state.blocked_sessions.contains(&session_id)) - }); - ix.and_then(|ix| state.runnables.remove(ix)) - }; - - if let Some(runnable) = runnable { - runnable.run(); - return true; - } - - false - } - - fn advance_clock_to_next_timer(&self) -> bool { - if let Some(timer) = self.state.lock().timers.first() { - self.clock.advance(timer.expiration - self.clock.now()); - true - } else { - false - } - } - - pub fn advance_clock(&self, duration: Duration) { - let next_now = self.clock.now() + duration; - loop { - self.run(); - if let Some(timer) = self.state.lock().timers.first() - && timer.expiration <= next_now - { - self.clock.advance(timer.expiration - self.clock.now()); - } else { - break; - } - } - self.clock.advance(next_now - self.clock.now()); - } - - fn park(&self, deadline: Option) -> bool { - if self.state.lock().allow_parking { - if let Some(deadline) = deadline { - let now = Instant::now(); - let timeout = deadline.saturating_duration_since(now); - thread::park_timeout(timeout); - now.elapsed() < timeout - } else { - thread::park(); - true - } - } else if deadline.is_some() { - false - } else if self.state.lock().capture_pending_traces { - let mut pending_traces = String::new(); - for (_, trace) in mem::take(&mut self.state.lock().pending_traces) { - writeln!(pending_traces, "{:?}", exclude_wakers_from_trace(trace)).unwrap(); - } - panic!("Parking forbidden. Pending traces:\n{}", pending_traces); - } else { - panic!( - "Parking forbidden. Re-run with {PENDING_TRACES_VAR_NAME}=1 to show pending traces" - ); - } - } -} - -impl Scheduler for TestScheduler { - /// Block until the given future completes, with an optional timeout. If the - /// future is unable to make progress at any moment before the timeout and - /// no other tasks or timers remain, we panic unless parking is allowed. If - /// parking is allowed, we block up to the timeout or indefinitely if none - /// is provided. This is to allow testing a mix of deterministic and - /// non-deterministic async behavior, such as when interacting with I/O in - /// an otherwise deterministic test. - fn block( - &self, - session_id: Option, - mut future: LocalBoxFuture<()>, - timeout: Option, - ) { - if let Some(session_id) = session_id { - self.state.lock().blocked_sessions.push(session_id); - } - - let deadline = timeout.map(|timeout| Instant::now() + timeout); - let awoken = Arc::new(AtomicBool::new(false)); - let waker = Box::new(TracingWaker { - id: None, - awoken: awoken.clone(), - thread: self.thread.clone(), - state: self.state.clone(), - }); - let waker = unsafe { Waker::new(Box::into_raw(waker) as *const (), &WAKER_VTABLE) }; - let max_ticks = if timeout.is_some() { - self.rng - .lock() - .random_range(self.state.lock().timeout_ticks.clone()) - } else { - usize::MAX - }; - let mut cx = Context::from_waker(&waker); - - for _ in 0..max_ticks { - let Poll::Pending = future.poll_unpin(&mut cx) else { - break; - }; - - let mut stepped = None; - while self.rng.lock().random() { - let stepped = stepped.get_or_insert(false); - if self.step() { - *stepped = true; - } else { - break; - } - } - - let stepped = stepped.unwrap_or(true); - let awoken = awoken.swap(false, SeqCst); - if !stepped && !awoken && !self.advance_clock_to_next_timer() { - if !self.park(deadline) { - break; - } - } - } - - if session_id.is_some() { - self.state.lock().blocked_sessions.pop(); - } - } - - fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable) { - let mut state = self.state.lock(); - let ix = if state.randomize_order { - let start_ix = state - .runnables - .iter() - .rposition(|task| task.session_id == Some(session_id)) - .map_or(0, |ix| ix + 1); - self.rng - .lock() - .random_range(start_ix..=state.runnables.len()) - } else { - state.runnables.len() - }; - state.runnables.insert( - ix, - ScheduledRunnable { - session_id: Some(session_id), - runnable, - }, - ); - drop(state); - self.thread.unpark(); - } - - fn schedule_background(&self, runnable: Runnable) { - let mut state = self.state.lock(); - let ix = if state.randomize_order { - self.rng.lock().random_range(0..=state.runnables.len()) - } else { - state.runnables.len() - }; - state.runnables.insert( - ix, - ScheduledRunnable { - session_id: None, - runnable, - }, - ); - drop(state); - self.thread.unpark(); - } - - fn timer(&self, duration: Duration) -> Timer { - let (tx, rx) = oneshot::channel(); - let state = &mut *self.state.lock(); - state.timers.push(ScheduledTimer { - expiration: self.clock.now() + duration, - _notify: tx, - }); - state.timers.sort_by_key(|timer| timer.expiration); - Timer(rx) - } - - fn clock(&self) -> Arc { - self.clock.clone() - } - - fn as_test(&self) -> &TestScheduler { - self - } -} - -#[derive(Clone, Debug)] -pub struct TestSchedulerConfig { - pub seed: u64, - pub randomize_order: bool, - pub allow_parking: bool, - pub capture_pending_traces: bool, - pub timeout_ticks: RangeInclusive, -} - -impl TestSchedulerConfig { - pub fn with_seed(seed: u64) -> Self { - Self { - seed, - ..Default::default() - } - } -} - -impl Default for TestSchedulerConfig { - fn default() -> Self { - Self { - seed: 0, - randomize_order: true, - allow_parking: false, - capture_pending_traces: env::var(PENDING_TRACES_VAR_NAME) - .map_or(false, |var| var == "1" || var == "true"), - timeout_ticks: 0..=1000, - } - } -} - -struct ScheduledRunnable { - session_id: Option, - runnable: Runnable, -} - -impl ScheduledRunnable { - fn run(self) { - self.runnable.run(); - } -} - -struct ScheduledTimer { - expiration: Instant, - _notify: oneshot::Sender<()>, -} - -struct SchedulerState { - runnables: VecDeque, - timers: Vec, - blocked_sessions: Vec, - randomize_order: bool, - allow_parking: bool, - timeout_ticks: RangeInclusive, - next_session_id: SessionId, - capture_pending_traces: bool, - next_trace_id: TraceId, - pending_traces: BTreeMap, -} - -const WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new( - TracingWaker::clone_raw, - TracingWaker::wake_raw, - TracingWaker::wake_by_ref_raw, - TracingWaker::drop_raw, -); - -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord)] -struct TraceId(usize); - -struct TracingWaker { - id: Option, - awoken: Arc, - thread: Thread, - state: Arc>, -} - -impl Clone for TracingWaker { - fn clone(&self) -> Self { - let mut state = self.state.lock(); - let id = if state.capture_pending_traces { - let id = state.next_trace_id; - state.next_trace_id.0 += 1; - state.pending_traces.insert(id, Backtrace::new_unresolved()); - Some(id) - } else { - None - }; - Self { - id, - awoken: self.awoken.clone(), - thread: self.thread.clone(), - state: self.state.clone(), - } - } -} - -impl Drop for TracingWaker { - fn drop(&mut self) { - if let Some(id) = self.id { - self.state.lock().pending_traces.remove(&id); - } - } -} - -impl TracingWaker { - fn wake(self) { - self.wake_by_ref(); - } - - fn wake_by_ref(&self) { - if let Some(id) = self.id { - self.state.lock().pending_traces.remove(&id); - } - self.awoken.store(true, SeqCst); - self.thread.unpark(); - } - - fn clone_raw(waker: *const ()) -> RawWaker { - let waker = waker as *const TracingWaker; - let waker = unsafe { &*waker }; - RawWaker::new( - Box::into_raw(Box::new(waker.clone())) as *const (), - &WAKER_VTABLE, - ) - } - - fn wake_raw(waker: *const ()) { - let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) }; - waker.wake(); - } - - fn wake_by_ref_raw(waker: *const ()) { - let waker = waker as *const TracingWaker; - let waker = unsafe { &*waker }; - waker.wake_by_ref(); - } - - fn drop_raw(waker: *const ()) { - let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) }; - drop(waker); - } -} - -pub struct Yield(usize); - -impl Future for Yield { - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - if self.0 == 0 { - Poll::Ready(()) - } else { - self.0 -= 1; - cx.waker().wake_by_ref(); - Poll::Pending - } - } -} - -fn exclude_wakers_from_trace(mut trace: Backtrace) -> Backtrace { - trace.resolve(); - let mut frames: Vec = trace.into(); - let waker_clone_frame_ix = frames.iter().position(|frame| { - frame.symbols().iter().any(|symbol| { - symbol - .name() - .is_some_and(|name| format!("{name:#?}") == type_name_of_val(&Waker::clone)) - }) - }); - - if let Some(waker_clone_frame_ix) = waker_clone_frame_ix { - frames.drain(..waker_clone_frame_ix + 1); - } - - Backtrace::from(frames) -} diff --git a/crates/scheduler/src/tests.rs b/crates/scheduler/src/tests.rs deleted file mode 100644 index 14f436c502..0000000000 --- a/crates/scheduler/src/tests.rs +++ /dev/null @@ -1,372 +0,0 @@ -use super::*; -use futures::{ - FutureExt, - channel::{mpsc, oneshot}, - executor::block_on, - future, - sink::SinkExt, - stream::{FuturesUnordered, StreamExt}, -}; -use std::{ - cell::RefCell, - collections::{BTreeSet, HashSet}, - pin::Pin, - rc::Rc, - sync::Arc, - task::{Context, Poll}, -}; - -#[test] -fn test_foreground_executor_spawn() { - let result = TestScheduler::once(async |scheduler| { - let task = scheduler.foreground().spawn(async move { 42 }); - task.await - }); - assert_eq!(result, 42); -} - -#[test] -fn test_background_executor_spawn() { - TestScheduler::once(async |scheduler| { - let task = scheduler.background().spawn(async move { 42 }); - let result = task.await; - assert_eq!(result, 42); - }); -} - -#[test] -fn test_foreground_ordering() { - let mut traces = HashSet::new(); - - TestScheduler::many(100, async |scheduler| { - #[derive(Hash, PartialEq, Eq)] - struct TraceEntry { - session: usize, - task: usize, - } - - let trace = Rc::new(RefCell::new(Vec::new())); - - let foreground_1 = scheduler.foreground(); - for task in 0..10 { - foreground_1 - .spawn({ - let trace = trace.clone(); - async move { - trace.borrow_mut().push(TraceEntry { session: 0, task }); - } - }) - .detach(); - } - - let foreground_2 = scheduler.foreground(); - for task in 0..10 { - foreground_2 - .spawn({ - let trace = trace.clone(); - async move { - trace.borrow_mut().push(TraceEntry { session: 1, task }); - } - }) - .detach(); - } - - scheduler.run(); - - assert_eq!( - trace - .borrow() - .iter() - .filter(|entry| entry.session == 0) - .map(|entry| entry.task) - .collect::>(), - (0..10).collect::>() - ); - assert_eq!( - trace - .borrow() - .iter() - .filter(|entry| entry.session == 1) - .map(|entry| entry.task) - .collect::>(), - (0..10).collect::>() - ); - - traces.insert(trace.take()); - }); - - assert!(traces.len() > 1, "Expected at least two traces"); -} - -#[test] -fn test_timer_ordering() { - TestScheduler::many(1, async |scheduler| { - let background = scheduler.background(); - let futures = FuturesUnordered::new(); - futures.push( - async { - background.timer(Duration::from_millis(100)).await; - 2 - } - .boxed(), - ); - futures.push( - async { - background.timer(Duration::from_millis(50)).await; - 1 - } - .boxed(), - ); - futures.push( - async { - background.timer(Duration::from_millis(150)).await; - 3 - } - .boxed(), - ); - assert_eq!(futures.collect::>().await, vec![1, 2, 3]); - }); -} - -#[test] -fn test_send_from_bg_to_fg() { - TestScheduler::once(async |scheduler| { - let foreground = scheduler.foreground(); - let background = scheduler.background(); - - let (sender, receiver) = oneshot::channel::(); - - background - .spawn(async move { - sender.send(42).unwrap(); - }) - .detach(); - - let task = foreground.spawn(async move { receiver.await.unwrap() }); - let result = task.await; - assert_eq!(result, 42); - }); -} - -#[test] -fn test_randomize_order() { - // Test deterministic mode: different seeds should produce same execution order - let mut deterministic_results = HashSet::new(); - for seed in 0..10 { - let config = TestSchedulerConfig { - seed, - randomize_order: false, - ..Default::default() - }; - let order = block_on(capture_execution_order(config)); - assert_eq!(order.len(), 6); - deterministic_results.insert(order); - } - - // All deterministic runs should produce the same result - assert_eq!( - deterministic_results.len(), - 1, - "Deterministic mode should always produce same execution order" - ); - - // Test randomized mode: different seeds can produce different execution orders - let mut randomized_results = HashSet::new(); - for seed in 0..20 { - let config = TestSchedulerConfig::with_seed(seed); - let order = block_on(capture_execution_order(config)); - assert_eq!(order.len(), 6); - randomized_results.insert(order); - } - - // Randomized mode should produce multiple different execution orders - assert!( - randomized_results.len() > 1, - "Randomized mode should produce multiple different orders" - ); -} - -async fn capture_execution_order(config: TestSchedulerConfig) -> Vec { - let scheduler = Arc::new(TestScheduler::new(config)); - let foreground = scheduler.foreground(); - let background = scheduler.background(); - - let (sender, receiver) = mpsc::unbounded::(); - - // Spawn foreground tasks - for i in 0..3 { - let mut sender = sender.clone(); - foreground - .spawn(async move { - sender.send(format!("fg-{}", i)).await.ok(); - }) - .detach(); - } - - // Spawn background tasks - for i in 0..3 { - let mut sender = sender.clone(); - background - .spawn(async move { - sender.send(format!("bg-{}", i)).await.ok(); - }) - .detach(); - } - - drop(sender); // Close sender to signal no more messages - scheduler.run(); - - receiver.collect().await -} - -#[test] -fn test_block() { - let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default())); - let (tx, rx) = oneshot::channel(); - - // Spawn background task to send value - let _ = scheduler - .background() - .spawn(async move { - tx.send(42).unwrap(); - }) - .detach(); - - // Block on receiving the value - let result = scheduler.foreground().block_on(async { rx.await.unwrap() }); - assert_eq!(result, 42); -} - -#[test] -#[should_panic(expected = "futures_channel::oneshot::Inner")] -fn test_parking_panics() { - let config = TestSchedulerConfig { - capture_pending_traces: true, - ..Default::default() - }; - let scheduler = Arc::new(TestScheduler::new(config)); - scheduler.foreground().block_on(async { - let (_tx, rx) = oneshot::channel::<()>(); - rx.await.unwrap(); // This will never complete - }); -} - -#[test] -fn test_block_with_parking() { - let config = TestSchedulerConfig { - allow_parking: true, - ..Default::default() - }; - let scheduler = Arc::new(TestScheduler::new(config)); - let (tx, rx) = oneshot::channel(); - - // Spawn background task to send value - let _ = scheduler - .background() - .spawn(async move { - tx.send(42).unwrap(); - }) - .detach(); - - // Block on receiving the value (will park if needed) - let result = scheduler.foreground().block_on(async { rx.await.unwrap() }); - assert_eq!(result, 42); -} - -#[test] -fn test_helper_methods() { - // Test the once method - let result = TestScheduler::once(async |scheduler: Arc| { - let background = scheduler.background(); - background.spawn(async { 42 }).await - }); - assert_eq!(result, 42); - - // Test the many method - let results = TestScheduler::many(3, async |scheduler: Arc| { - let background = scheduler.background(); - background.spawn(async { 10 }).await - }); - assert_eq!(results, vec![10, 10, 10]); -} - -#[test] -fn test_block_with_timeout() { - // Test case: future completes within timeout - TestScheduler::once(async |scheduler| { - let foreground = scheduler.foreground(); - let future = future::ready(42); - let output = foreground.block_with_timeout(Duration::from_millis(100), future); - assert_eq!(output.unwrap(), 42); - }); - - // Test case: future times out - TestScheduler::once(async |scheduler| { - let foreground = scheduler.foreground(); - let future = future::pending::<()>(); - let output = foreground.block_with_timeout(Duration::from_millis(50), future); - let _ = output.expect_err("future should not have finished"); - }); - - // Test case: future makes progress via timer but still times out - let mut results = BTreeSet::new(); - TestScheduler::many(100, async |scheduler| { - let task = scheduler.background().spawn(async move { - Yield { polls: 10 }.await; - 42 - }); - let output = scheduler - .foreground() - .block_with_timeout(Duration::from_millis(50), task); - results.insert(output.ok()); - }); - assert_eq!( - results.into_iter().collect::>(), - vec![None, Some(42)] - ); -} - -// When calling block, we shouldn't make progress on foreground-spawned futures with the same session id. -#[test] -fn test_block_does_not_progress_same_session_foreground() { - let mut task2_made_progress_once = false; - TestScheduler::many(1000, async |scheduler| { - let foreground1 = scheduler.foreground(); - let foreground2 = scheduler.foreground(); - - let task1 = foreground1.spawn(async move {}); - let task2 = foreground2.spawn(async move {}); - - foreground1.block_on(async { - scheduler.yield_random().await; - assert!(!task1.is_ready()); - task2_made_progress_once |= task2.is_ready(); - }); - - task1.await; - task2.await; - }); - - assert!( - task2_made_progress_once, - "Expected task from different foreground executor to make progress (at least once)" - ); -} - -struct Yield { - polls: usize, -} - -impl Future for Yield { - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - self.polls -= 1; - if self.polls == 0 { - Poll::Ready(()) - } else { - cx.waker().wake_by_ref(); - Poll::Pending - } - } -} diff --git a/crates/sum_tree/Cargo.toml b/crates/sum_tree/Cargo.toml deleted file mode 100644 index d9924a4210..0000000000 --- a/crates/sum_tree/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "sum_tree" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "A sum tree data structure, a concurrency-friendly B-tree" - -[lints] -workspace = true - -[lib] -path = "src/sum_tree.rs" -doctest = false - -[dependencies] -arrayvec = "0.7.1" -rayon.workspace = true -log.workspace = true -tracing.workspace = true - -[dev-dependencies] -ctor.workspace = true -rand.workspace = true - -[package.metadata.cargo-machete] -ignored = ["tracing"] diff --git a/crates/sum_tree/LICENSE-APACHE b/crates/sum_tree/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/sum_tree/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs deleted file mode 100644 index efdb6ac13a..0000000000 --- a/crates/sum_tree/src/cursor.rs +++ /dev/null @@ -1,832 +0,0 @@ -use super::*; -use arrayvec::ArrayVec; -use std::{cmp::Ordering, mem, sync::Arc}; -use tracing::instrument; - -#[derive(Clone)] -struct StackEntry<'a, T: Item, D> { - tree: &'a SumTree, - index: u32, - position: D, -} - -impl<'a, T: Item, D> StackEntry<'a, T, D> { - #[inline] - fn index(&self) -> usize { - self.index as usize - } -} - -impl fmt::Debug for StackEntry<'_, T, D> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("StackEntry") - .field("index", &self.index) - .field("position", &self.position) - .finish() - } -} - -#[derive(Clone)] -pub struct Cursor<'a, 'b, T: Item, D> { - tree: &'a SumTree, - stack: ArrayVec, 16>, - position: D, - did_seek: bool, - at_end: bool, - cx: ::Context<'b>, -} - -impl fmt::Debug for Cursor<'_, '_, T, D> -where - T::Summary: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Cursor") - .field("tree", &self.tree) - .field("stack", &self.stack) - .field("position", &self.position) - .field("did_seek", &self.did_seek) - .field("at_end", &self.at_end) - .finish() - } -} - -pub struct Iter<'a, T: Item> { - tree: &'a SumTree, - stack: ArrayVec, 16>, -} - -impl<'a, 'b, T, D> Cursor<'a, 'b, T, D> -where - T: Item, - D: Dimension<'a, T::Summary>, -{ - pub fn new(tree: &'a SumTree, cx: ::Context<'b>) -> Self { - Self { - tree, - stack: ArrayVec::new(), - position: D::zero(cx), - did_seek: false, - at_end: tree.is_empty(), - cx, - } - } - - fn reset(&mut self) { - self.did_seek = false; - self.at_end = self.tree.is_empty(); - self.stack.truncate(0); - self.position = D::zero(self.cx); - } - - pub fn start(&self) -> &D { - &self.position - } - - #[track_caller] - pub fn end(&self) -> D { - if let Some(item_summary) = self.item_summary() { - let mut end = self.start().clone(); - end.add_summary(item_summary, self.cx); - end - } else { - self.start().clone() - } - } - - /// Item is None, when the list is empty, or this cursor is at the end of the list. - #[track_caller] - pub fn item(&self) -> Option<&'a T> { - self.assert_did_seek(); - if let Some(entry) = self.stack.last() { - match *entry.tree.0 { - Node::Leaf { ref items, .. } => { - if entry.index() == items.len() { - None - } else { - Some(&items[entry.index()]) - } - } - _ => unreachable!(), - } - } else { - None - } - } - - #[track_caller] - pub fn item_summary(&self) -> Option<&'a T::Summary> { - self.assert_did_seek(); - if let Some(entry) = self.stack.last() { - match *entry.tree.0 { - Node::Leaf { - ref item_summaries, .. - } => { - if entry.index() == item_summaries.len() { - None - } else { - Some(&item_summaries[entry.index()]) - } - } - _ => unreachable!(), - } - } else { - None - } - } - - #[track_caller] - 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() - } - } - - #[track_caller] - 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 - } - - #[track_caller] - pub fn prev_item(&self) -> Option<&'a T> { - self.assert_did_seek(); - if let Some(entry) = self.stack.last() { - if entry.index() == 0 { - if let Some(prev_leaf) = self.prev_leaf() { - Some(prev_leaf.0.items().last().unwrap()) - } else { - None - } - } else { - match *entry.tree.0 { - Node::Leaf { ref items, .. } => Some(&items[entry.index() - 1]), - _ => unreachable!(), - } - } - } else if self.at_end { - self.tree.last() - } else { - None - } - } - - #[track_caller] - fn prev_leaf(&self) -> Option<&'a SumTree> { - for entry in self.stack.iter().rev().skip(1) { - if entry.index() != 0 { - match *entry.tree.0 { - Node::Internal { - ref child_trees, .. - } => return Some(child_trees[entry.index() - 1].rightmost_leaf()), - Node::Leaf { .. } => unreachable!(), - }; - } - } - None - } - - #[track_caller] - #[instrument(skip_all)] - pub fn prev(&mut self) { - self.search_backward(|_| true) - } - - #[track_caller] - pub fn search_backward(&mut self, mut filter_node: F) - where - F: FnMut(&T::Summary) -> bool, - { - if !self.did_seek { - self.did_seek = true; - self.at_end = true; - } - - if self.at_end { - self.position = D::zero(self.cx); - self.at_end = self.tree.is_empty(); - if !self.tree.is_empty() { - self.stack.push(StackEntry { - tree: self.tree, - index: self.tree.0.child_summaries().len() as u32, - position: D::from_summary(self.tree.summary(), self.cx), - }); - } - } - - let mut descending = false; - while !self.stack.is_empty() { - if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) { - self.position = position.clone(); - } else { - self.position = D::zero(self.cx); - } - - let entry = self.stack.last_mut().unwrap(); - if !descending { - if entry.index() == 0 { - self.stack.pop(); - continue; - } else { - entry.index -= 1; - } - } - - for summary in &entry.tree.0.child_summaries()[..entry.index()] { - self.position.add_summary(summary, self.cx); - } - entry.position = self.position.clone(); - - descending = filter_node(&entry.tree.0.child_summaries()[entry.index()]); - match entry.tree.0.as_ref() { - Node::Internal { child_trees, .. } => { - if descending { - let tree = &child_trees[entry.index()]; - self.stack.push(StackEntry { - position: D::zero(self.cx), - tree, - index: tree.0.child_summaries().len() as u32 - 1, - }) - } - } - Node::Leaf { .. } => { - if descending { - break; - } - } - } - } - } - - #[track_caller] - pub fn next(&mut self) { - self.search_forward(|_| true) - } - - #[track_caller] - pub fn search_forward(&mut self, mut filter_node: F) - where - F: FnMut(&T::Summary) -> bool, - { - let mut descend = false; - - if self.stack.is_empty() { - if !self.at_end { - self.stack.push(StackEntry { - tree: self.tree, - index: 0, - position: D::zero(self.cx), - }); - descend = true; - } - self.did_seek = true; - } - - while !self.stack.is_empty() { - let new_subtree = { - let entry = self.stack.last_mut().unwrap(); - match entry.tree.0.as_ref() { - Node::Internal { - child_trees, - child_summaries, - .. - } => { - if !descend { - entry.index += 1; - entry.position = self.position.clone(); - } - - while entry.index() < child_summaries.len() { - let next_summary = &child_summaries[entry.index()]; - if filter_node(next_summary) { - break; - } else { - entry.index += 1; - entry.position.add_summary(next_summary, self.cx); - self.position.add_summary(next_summary, self.cx); - } - } - - child_trees.get(entry.index()) - } - Node::Leaf { item_summaries, .. } => { - if !descend { - let item_summary = &item_summaries[entry.index()]; - entry.index += 1; - entry.position.add_summary(item_summary, self.cx); - self.position.add_summary(item_summary, self.cx); - } - - loop { - if let Some(next_item_summary) = item_summaries.get(entry.index()) { - if filter_node(next_item_summary) { - return; - } else { - entry.index += 1; - entry.position.add_summary(next_item_summary, self.cx); - self.position.add_summary(next_item_summary, self.cx); - } - } else { - break None; - } - } - } - } - }; - - if let Some(subtree) = new_subtree { - descend = true; - self.stack.push(StackEntry { - tree: subtree, - index: 0, - position: self.position.clone(), - }); - } else { - descend = false; - self.stack.pop(); - } - } - - self.at_end = self.stack.is_empty(); - debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); - } - - #[track_caller] - fn assert_did_seek(&self) { - assert!( - self.did_seek, - "Must call `seek`, `next` or `prev` before calling this method" - ); - } - - pub fn did_seek(&self) -> bool { - self.did_seek - } -} - -impl<'a, 'b, T, D> Cursor<'a, 'b, T, D> -where - T: Item, - D: Dimension<'a, T::Summary>, -{ - /// Returns whether we found the item you were seeking for. - #[track_caller] - #[instrument(skip_all)] - pub fn seek(&mut self, pos: &Target, bias: Bias) -> bool - where - Target: SeekTarget<'a, T::Summary, D>, - { - self.reset(); - self.seek_internal(pos, bias, &mut ()) - } - - /// Returns whether we found the item you were seeking for. - /// - /// # Panics - /// - /// If we did not seek before, use seek instead in that case. - #[track_caller] - #[instrument(skip_all)] - pub fn seek_forward(&mut self, pos: &Target, bias: Bias) -> bool - where - Target: SeekTarget<'a, T::Summary, D>, - { - self.seek_internal(pos, bias, &mut ()) - } - - /// Advances the cursor and returns traversed items as a tree. - #[track_caller] - pub fn slice(&mut self, end: &Target, bias: Bias) -> SumTree - where - Target: SeekTarget<'a, T::Summary, D>, - { - let mut slice = SliceSeekAggregate { - tree: SumTree::new(self.cx), - leaf_items: ArrayVec::new(), - leaf_item_summaries: ArrayVec::new(), - leaf_summary: ::zero(self.cx), - }; - self.seek_internal(end, bias, &mut slice); - slice.tree - } - - #[track_caller] - pub fn suffix(&mut self) -> SumTree { - self.slice(&End::new(), Bias::Right) - } - - #[track_caller] - pub fn summary(&mut self, end: &Target, bias: Bias) -> Output - where - Target: SeekTarget<'a, T::Summary, D>, - Output: Dimension<'a, T::Summary>, - { - let mut summary = SummarySeekAggregate(Output::zero(self.cx)); - self.seek_internal(end, bias, &mut summary); - summary.0 - } - - /// Returns whether we found the item you were seeking for. - #[track_caller] - #[instrument(skip_all)] - fn seek_internal( - &mut self, - target: &dyn SeekTarget<'a, T::Summary, D>, - bias: Bias, - aggregate: &mut dyn SeekAggregate<'a, T>, - ) -> bool { - assert!( - target.cmp(&self.position, self.cx).is_ge(), - "cannot seek backward", - ); - - if !self.did_seek { - self.did_seek = true; - self.stack.push(StackEntry { - tree: self.tree, - index: 0, - position: D::zero(self.cx), - }); - } - - let mut ascending = false; - 'outer: while let Some(entry) = self.stack.last_mut() { - match *entry.tree.0 { - Node::Internal { - ref child_summaries, - ref child_trees, - .. - } => { - if ascending { - entry.index += 1; - entry.position = self.position.clone(); - } - - for (child_tree, child_summary) in child_trees[entry.index()..] - .iter() - .zip(&child_summaries[entry.index()..]) - { - let mut child_end = self.position.clone(); - child_end.add_summary(child_summary, self.cx); - - let comparison = target.cmp(&child_end, self.cx); - if comparison == Ordering::Greater - || (comparison == Ordering::Equal && bias == Bias::Right) - { - self.position = child_end; - aggregate.push_tree(child_tree, child_summary, self.cx); - entry.index += 1; - entry.position = self.position.clone(); - } else { - self.stack.push(StackEntry { - tree: child_tree, - index: 0, - position: self.position.clone(), - }); - ascending = false; - continue 'outer; - } - } - } - Node::Leaf { - ref items, - ref item_summaries, - .. - } => { - aggregate.begin_leaf(); - - for (item, item_summary) in items[entry.index()..] - .iter() - .zip(&item_summaries[entry.index()..]) - { - let mut child_end = self.position.clone(); - child_end.add_summary(item_summary, self.cx); - - let comparison = target.cmp(&child_end, self.cx); - if comparison == Ordering::Greater - || (comparison == Ordering::Equal && bias == Bias::Right) - { - self.position = child_end; - aggregate.push_item(item, item_summary, self.cx); - entry.index += 1; - } else { - aggregate.end_leaf(self.cx); - break 'outer; - } - } - - aggregate.end_leaf(self.cx); - } - } - - self.stack.pop(); - ascending = true; - } - - self.at_end = self.stack.is_empty(); - debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); - - let mut end = self.position.clone(); - if bias == Bias::Left - && let Some(summary) = self.item_summary() - { - end.add_summary(summary, self.cx); - } - - target.cmp(&end, self.cx) == Ordering::Equal - } -} - -impl<'a, T: Item> Iter<'a, T> { - pub(crate) fn new(tree: &'a SumTree) -> Self { - Self { - tree, - stack: Default::default(), - } - } -} - -impl<'a, T: Item> Iterator for Iter<'a, T> { - type Item = &'a T; - - fn next(&mut self) -> Option { - let mut descend = false; - - if self.stack.is_empty() { - self.stack.push(StackEntry { - tree: self.tree, - index: 0, - position: (), - }); - descend = true; - } - - while !self.stack.is_empty() { - let new_subtree = { - let entry = self.stack.last_mut().unwrap(); - match entry.tree.0.as_ref() { - Node::Internal { child_trees, .. } => { - if !descend { - entry.index += 1; - } - child_trees.get(entry.index()) - } - Node::Leaf { items, .. } => { - if !descend { - entry.index += 1; - } - - if let Some(next_item) = items.get(entry.index()) { - return Some(next_item); - } else { - None - } - } - } - }; - - if let Some(subtree) = new_subtree { - descend = true; - self.stack.push(StackEntry { - tree: subtree, - index: 0, - position: (), - }); - } else { - descend = false; - self.stack.pop(); - } - } - - None - } -} - -impl<'a, 'b, T: Item, D> Iterator for Cursor<'a, 'b, T, D> -where - D: Dimension<'a, T::Summary>, -{ - type Item = &'a T; - - fn next(&mut self) -> Option { - if !self.did_seek { - self.next(); - } - - if let Some(item) = self.item() { - self.next(); - Some(item) - } else { - None - } - } -} - -pub struct FilterCursor<'a, 'b, F, T: Item, D> { - cursor: Cursor<'a, 'b, T, D>, - filter_node: F, -} - -impl<'a, 'b, F, T: Item, D> FilterCursor<'a, 'b, F, T, D> -where - F: FnMut(&T::Summary) -> bool, - T: Item, - D: Dimension<'a, T::Summary>, -{ - pub fn new( - tree: &'a SumTree, - cx: ::Context<'b>, - filter_node: F, - ) -> Self { - let cursor = tree.cursor::(cx); - Self { - cursor, - filter_node, - } - } - - pub fn start(&self) -> &D { - self.cursor.start() - } - - pub fn end(&self) -> D { - self.cursor.end() - } - - pub fn item(&self) -> Option<&'a T> { - self.cursor.item() - } - - pub fn item_summary(&self) -> Option<&'a T::Summary> { - self.cursor.item_summary() - } - - pub fn next(&mut self) { - self.cursor.search_forward(&mut self.filter_node); - } - - pub fn prev(&mut self) { - self.cursor.search_backward(&mut self.filter_node); - } -} - -impl<'a, 'b, F, T: Item, U> Iterator for FilterCursor<'a, 'b, F, T, U> -where - F: FnMut(&T::Summary) -> bool, - U: Dimension<'a, T::Summary>, -{ - type Item = &'a T; - - fn next(&mut self) -> Option { - if !self.cursor.did_seek { - self.next(); - } - - if let Some(item) = self.item() { - self.cursor.search_forward(&mut self.filter_node); - Some(item) - } else { - None - } - } -} - -trait SeekAggregate<'a, T: Item> { - fn begin_leaf(&mut self); - fn end_leaf(&mut self, cx: ::Context<'_>); - fn push_item( - &mut self, - item: &'a T, - summary: &'a T::Summary, - cx: ::Context<'_>, - ); - fn push_tree( - &mut self, - tree: &'a SumTree, - summary: &'a T::Summary, - cx: ::Context<'_>, - ); -} - -struct SliceSeekAggregate { - tree: SumTree, - leaf_items: ArrayVec, - leaf_item_summaries: ArrayVec, - leaf_summary: T::Summary, -} - -struct SummarySeekAggregate(D); - -impl SeekAggregate<'_, T> for () { - fn begin_leaf(&mut self) {} - fn end_leaf(&mut self, _: ::Context<'_>) {} - fn push_item(&mut self, _: &T, _: &T::Summary, _: ::Context<'_>) {} - fn push_tree( - &mut self, - _: &SumTree, - _: &T::Summary, - _: ::Context<'_>, - ) { - } -} - -impl SeekAggregate<'_, T> for SliceSeekAggregate { - fn begin_leaf(&mut self) {} - fn end_leaf(&mut self, cx: ::Context<'_>) { - self.tree.append( - SumTree(Arc::new(Node::Leaf { - summary: mem::replace(&mut self.leaf_summary, ::zero(cx)), - items: mem::take(&mut self.leaf_items), - item_summaries: mem::take(&mut self.leaf_item_summaries), - })), - cx, - ); - } - fn push_item( - &mut self, - item: &T, - summary: &T::Summary, - cx: ::Context<'_>, - ) { - self.leaf_items.push(item.clone()); - self.leaf_item_summaries.push(summary.clone()); - Summary::add_summary(&mut self.leaf_summary, summary, cx); - } - fn push_tree( - &mut self, - tree: &SumTree, - _: &T::Summary, - cx: ::Context<'_>, - ) { - self.tree.append(tree.clone(), cx); - } -} - -impl<'a, T: Item, D> SeekAggregate<'a, T> for SummarySeekAggregate -where - D: Dimension<'a, T::Summary>, -{ - fn begin_leaf(&mut self) {} - fn end_leaf(&mut self, _: ::Context<'_>) {} - fn push_item( - &mut self, - _: &T, - summary: &'a T::Summary, - cx: ::Context<'_>, - ) { - self.0.add_summary(summary, cx); - } - fn push_tree( - &mut self, - _: &SumTree, - summary: &'a T::Summary, - cx: ::Context<'_>, - ) { - self.0.add_summary(summary, cx); - } -} - -struct End(PhantomData); - -impl End { - fn new() -> Self { - Self(PhantomData) - } -} - -impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End { - fn cmp(&self, _: &D, _: S::Context<'_>) -> Ordering { - Ordering::Greater - } -} - -impl fmt::Debug for End { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("End").finish() - } -} diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs deleted file mode 100644 index 9de7ee9741..0000000000 --- a/crates/sum_tree/src/sum_tree.rs +++ /dev/null @@ -1,1738 +0,0 @@ -mod cursor; -mod tree_map; - -use arrayvec::ArrayVec; -pub use cursor::{Cursor, FilterCursor, Iter}; -use rayon::iter::{IndexedParallelIterator, IntoParallelIterator, ParallelIterator as _}; -use std::marker::PhantomData; -use std::mem; -use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc}; -pub use tree_map::{MapSeekTarget, TreeMap, TreeSet}; -use tracing::instrument; - -#[cfg(test)] -pub const TREE_BASE: usize = 2; -#[cfg(not(test))] -pub const TREE_BASE: usize = 6; - -/// An item that can be stored in a [`SumTree`] -/// -/// Must be summarized by a type that implements [`Summary`] -pub trait Item: Clone { - type Summary: Summary; - - fn summary(&self, cx: ::Context<'_>) -> Self::Summary; -} - -/// An [`Item`] whose summary has a specific key that can be used to identify it -pub trait KeyedItem: Item { - type Key: for<'a> Dimension<'a, Self::Summary> + Ord; - - fn key(&self) -> Self::Key; -} - -/// A type that describes the Sum of all [`Item`]s in a subtree of the [`SumTree`] -/// -/// Each Summary type can have multiple [`Dimension`]s that it measures, -/// which can be used to navigate the tree -pub trait Summary: Clone { - type Context<'a>: Copy; - fn zero<'a>(cx: Self::Context<'a>) -> Self; - fn add_summary<'a>(&mut self, summary: &Self, cx: Self::Context<'a>); -} - -pub trait ContextLessSummary: Clone { - fn zero() -> Self; - fn add_summary(&mut self, summary: &Self); -} - -impl Summary for T { - type Context<'a> = (); - - fn zero<'a>((): ()) -> Self { - T::zero() - } - - fn add_summary<'a>(&mut self, summary: &Self, (): ()) { - T::add_summary(self, summary) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct NoSummary; - -/// Catch-all implementation for when you need something that implements [`Summary`] without a specific type. -/// We implement it on a `NoSummary` instead of re-using `()`, as that avoids blanket impl collisions with `impl Dimension for T` -/// (as we also need unit type to be a fill-in dimension) -impl ContextLessSummary for NoSummary { - fn zero() -> Self { - NoSummary - } - - fn add_summary(&mut self, _: &Self) {} -} - -/// Each [`Summary`] type can have more than one [`Dimension`] type that it measures. -/// -/// You can use dimensions to seek to a specific location in the [`SumTree`] -/// -/// # Example: -/// Zed's rope has a `TextSummary` type that summarizes lines, characters, and bytes. -/// Each of these are different dimensions we may want to seek to -pub trait Dimension<'a, S: Summary>: Clone { - fn zero(cx: S::Context<'_>) -> Self; - - fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>); - #[must_use] - fn with_added_summary(mut self, summary: &'a S, cx: S::Context<'_>) -> Self { - self.add_summary(summary, cx); - self - } - - fn from_summary(summary: &'a S, cx: S::Context<'_>) -> Self { - let mut dimension = Self::zero(cx); - dimension.add_summary(summary, cx); - dimension - } -} - -impl<'a, T: Summary> Dimension<'a, T> for T { - fn zero(cx: T::Context<'_>) -> Self { - Summary::zero(cx) - } - - fn add_summary(&mut self, summary: &'a T, cx: T::Context<'_>) { - Summary::add_summary(self, summary, cx); - } -} - -pub trait SeekTarget<'a, S: Summary, D: Dimension<'a, S>> { - fn cmp(&self, cursor_location: &D, cx: S::Context<'_>) -> Ordering; -} - -impl<'a, S: Summary, D: Dimension<'a, S> + Ord> SeekTarget<'a, S, D> for D { - fn cmp(&self, cursor_location: &Self, _: S::Context<'_>) -> Ordering { - Ord::cmp(self, cursor_location) - } -} - -impl<'a, T: Summary> Dimension<'a, T> for () { - fn zero(_: T::Context<'_>) -> Self {} - - fn add_summary(&mut self, _: &'a T, _: T::Context<'_>) {} -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -pub struct Dimensions(pub D1, pub D2, pub D3); - -impl<'a, T: Summary, D1: Dimension<'a, T>, D2: Dimension<'a, T>, D3: Dimension<'a, T>> - Dimension<'a, T> for Dimensions -{ - fn zero(cx: T::Context<'_>) -> Self { - Dimensions(D1::zero(cx), D2::zero(cx), D3::zero(cx)) - } - - fn add_summary(&mut self, summary: &'a T, cx: T::Context<'_>) { - self.0.add_summary(summary, cx); - self.1.add_summary(summary, cx); - self.2.add_summary(summary, cx); - } -} - -impl<'a, S, D1, D2, D3> SeekTarget<'a, S, Dimensions> for D1 -where - S: Summary, - D1: SeekTarget<'a, S, D1> + Dimension<'a, S>, - D2: Dimension<'a, S>, - D3: Dimension<'a, S>, -{ - fn cmp(&self, cursor_location: &Dimensions, cx: S::Context<'_>) -> Ordering { - self.cmp(&cursor_location.0, cx) - } -} - -/// Bias is used to settle ambiguities when determining positions in an ordered sequence. -/// -/// The primary use case is for text, where Bias influences -/// which character an offset or anchor is associated with. -/// -/// # Examples -/// Given the buffer `AˇBCD`: -/// - The offset of the cursor is 1 -/// - [Bias::Left] would attach the cursor to the character `A` -/// - [Bias::Right] would attach the cursor to the character `B` -/// -/// Given the buffer `A«BCˇ»D`: -/// - The offset of the cursor is 3, and the selection is from 1 to 3 -/// - The left anchor of the selection has [Bias::Right], attaching it to the character `B` -/// - The right anchor of the selection has [Bias::Left], attaching it to the character `C` -/// -/// Given the buffer `{ˇ<...>`, where `<...>` is a folded region: -/// - The display offset of the cursor is 1, but the offset in the buffer is determined by the bias -/// - [Bias::Left] would attach the cursor to the character `{`, with a buffer offset of 1 -/// - [Bias::Right] would attach the cursor to the first character of the folded region, -/// and the buffer offset would be the offset of the first character of the folded region -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)] -pub enum Bias { - /// Attach to the character on the left - #[default] - Left, - /// Attach to the character on the right - Right, -} - -impl Bias { - pub fn invert(self) -> Self { - match self { - Self::Left => Self::Right, - Self::Right => Self::Left, - } - } -} - -/// A B+ tree in which each leaf node contains `Item`s of type `T` and a `Summary`s for each `Item`. -/// Each internal node contains a `Summary` of the items in its subtree. -/// -/// The maximum number of items per node is `TREE_BASE * 2`. -/// -/// Any [`Dimension`] supported by the [`Summary`] type can be used to seek to a specific location in the tree. -#[derive(Clone)] -pub struct SumTree(Arc>); - -impl fmt::Debug for SumTree -where - T: fmt::Debug + Item, - T::Summary: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_tuple("SumTree").field(&self.0).finish() - } -} - -impl SumTree { - pub fn new(cx: ::Context<'_>) -> Self { - SumTree(Arc::new(Node::Leaf { - summary: ::zero(cx), - items: ArrayVec::new(), - item_summaries: ArrayVec::new(), - })) - } - - /// Useful in cases where the item type has a non-trivial context type, but the zero value of the summary type doesn't depend on that context. - pub fn from_summary(summary: T::Summary) -> Self { - SumTree(Arc::new(Node::Leaf { - summary, - items: ArrayVec::new(), - item_summaries: ArrayVec::new(), - })) - } - - pub fn from_item(item: T, cx: ::Context<'_>) -> Self { - let mut tree = Self::new(cx); - tree.push(item, cx); - tree - } - - pub fn from_iter>( - iter: I, - cx: ::Context<'_>, - ) -> Self { - let mut nodes = Vec::new(); - - let mut iter = iter.into_iter().fuse().peekable(); - while iter.peek().is_some() { - let items: ArrayVec = iter.by_ref().take(2 * TREE_BASE).collect(); - let item_summaries: ArrayVec = - items.iter().map(|item| item.summary(cx)).collect(); - - let mut summary = item_summaries[0].clone(); - for item_summary in &item_summaries[1..] { - ::add_summary(&mut summary, item_summary, cx); - } - - nodes.push(Node::Leaf { - summary, - items, - item_summaries, - }); - } - - let mut parent_nodes = Vec::new(); - let mut height = 0; - while nodes.len() > 1 { - height += 1; - let mut current_parent_node = None; - for child_node in nodes.drain(..) { - let parent_node = current_parent_node.get_or_insert_with(|| Node::Internal { - summary: ::zero(cx), - height, - child_summaries: ArrayVec::new(), - child_trees: ArrayVec::new(), - }); - let Node::Internal { - summary, - child_summaries, - child_trees, - .. - } = parent_node - else { - unreachable!() - }; - let child_summary = child_node.summary(); - ::add_summary(summary, child_summary, cx); - child_summaries.push(child_summary.clone()); - child_trees.push(Self(Arc::new(child_node))); - - if child_trees.len() == 2 * TREE_BASE { - parent_nodes.extend(current_parent_node.take()); - } - } - parent_nodes.extend(current_parent_node.take()); - mem::swap(&mut nodes, &mut parent_nodes); - } - - if nodes.is_empty() { - Self::new(cx) - } else { - debug_assert_eq!(nodes.len(), 1); - Self(Arc::new(nodes.pop().unwrap())) - } - } - - pub fn from_par_iter(iter: I, cx: ::Context<'_>) -> Self - where - I: IntoParallelIterator, - Iter: IndexedParallelIterator, - T: Send + Sync, - T::Summary: Send + Sync, - for<'a> ::Context<'a>: Sync, - { - let mut nodes = iter - .into_par_iter() - .chunks(2 * TREE_BASE) - .map(|items| { - let items: ArrayVec = items.into_iter().collect(); - let item_summaries: ArrayVec = - items.iter().map(|item| item.summary(cx)).collect(); - let mut summary = item_summaries[0].clone(); - for item_summary in &item_summaries[1..] { - ::add_summary(&mut summary, item_summary, cx); - } - SumTree(Arc::new(Node::Leaf { - summary, - items, - item_summaries, - })) - }) - .collect::>(); - - let mut height = 0; - while nodes.len() > 1 { - height += 1; - nodes = nodes - .into_par_iter() - .chunks(2 * TREE_BASE) - .map(|child_nodes| { - let child_trees: ArrayVec, { 2 * TREE_BASE }> = - child_nodes.into_iter().collect(); - let child_summaries: ArrayVec = child_trees - .iter() - .map(|child_tree| child_tree.summary().clone()) - .collect(); - let mut summary = child_summaries[0].clone(); - for child_summary in &child_summaries[1..] { - ::add_summary(&mut summary, child_summary, cx); - } - SumTree(Arc::new(Node::Internal { - height, - summary, - child_summaries, - child_trees, - })) - }) - .collect::>(); - } - - if nodes.is_empty() { - Self::new(cx) - } else { - debug_assert_eq!(nodes.len(), 1); - nodes.pop().unwrap() - } - } - - #[allow(unused)] - pub fn items<'a>(&'a self, cx: ::Context<'a>) -> Vec { - let mut items = Vec::new(); - let mut cursor = self.cursor::<()>(cx); - cursor.next(); - while let Some(item) = cursor.item() { - items.push(item.clone()); - cursor.next(); - } - items - } - - pub fn iter(&self) -> Iter<'_, T> { - Iter::new(self) - } - - /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()`. - /// - /// Only returns the item that exactly has the target match. - #[instrument(skip_all)] - pub fn find_exact<'a, 'slf, D, Target>( - &'slf self, - cx: ::Context<'a>, - target: &Target, - bias: Bias, - ) -> (D, D, Option<&'slf T>) - where - D: Dimension<'slf, T::Summary>, - Target: SeekTarget<'slf, T::Summary, D>, - { - let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); - let comparison = target.cmp(&tree_end, cx); - if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) - { - return (tree_end.clone(), tree_end, None); - } - - let mut pos = D::zero(cx); - return match Self::find_recurse::<_, _, true>(cx, target, bias, &mut pos, self) { - Some((item, end)) => (pos, end, Some(item)), - None => (pos.clone(), pos, None), - }; - } - - /// A more efficient version of `Cursor::new()` + `Cursor::seek()` + `Cursor::item()` - #[instrument(skip_all)] - pub fn find<'a, 'slf, D, Target>( - &'slf self, - cx: ::Context<'a>, - target: &Target, - bias: Bias, - ) -> (D, D, Option<&'slf T>) - where - D: Dimension<'slf, T::Summary>, - Target: SeekTarget<'slf, T::Summary, D>, - { - let tree_end = D::zero(cx).with_added_summary(self.summary(), cx); - let comparison = target.cmp(&tree_end, cx); - if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) - { - return (tree_end.clone(), tree_end, None); - } - - let mut pos = D::zero(cx); - return match Self::find_recurse::<_, _, false>(cx, target, bias, &mut pos, self) { - Some((item, end)) => (pos, end, Some(item)), - None => (pos.clone(), pos, None), - }; - } - - fn find_recurse<'tree, 'a, D, Target, const EXACT: bool>( - cx: ::Context<'a>, - target: &Target, - bias: Bias, - position: &mut D, - this: &'tree SumTree, - ) -> Option<(&'tree T, D)> - where - D: Dimension<'tree, T::Summary>, - Target: SeekTarget<'tree, T::Summary, D>, - { - match &*this.0 { - Node::Internal { - child_summaries, - child_trees, - .. - } => { - for (child_tree, child_summary) in child_trees.iter().zip(child_summaries) { - let child_end = position.clone().with_added_summary(child_summary, cx); - - let comparison = target.cmp(&child_end, cx); - let target_in_child = comparison == Ordering::Less - || (comparison == Ordering::Equal && bias == Bias::Left); - if target_in_child { - return Self::find_recurse::( - cx, target, bias, position, child_tree, - ); - } - *position = child_end; - } - } - Node::Leaf { - items, - item_summaries, - .. - } => { - for (item, item_summary) in items.iter().zip(item_summaries) { - let mut child_end = position.clone(); - child_end.add_summary(item_summary, cx); - - let comparison = target.cmp(&child_end, cx); - let entry_found = if EXACT { - comparison == Ordering::Equal - } else { - comparison == Ordering::Less - || (comparison == Ordering::Equal && bias == Bias::Left) - }; - if entry_found { - return Some((item, child_end)); - } - - *position = child_end; - } - } - } - None - } - - pub fn cursor<'a, 'b, D>( - &'a self, - cx: ::Context<'b>, - ) -> Cursor<'a, 'b, T, D> - where - D: Dimension<'a, T::Summary>, - { - Cursor::new(self, cx) - } - - /// Note: If the summary type requires a non `()` context, then the filter cursor - /// that is returned cannot be used with Rust's iterators. - pub fn filter<'a, 'b, F, U>( - &'a self, - cx: ::Context<'b>, - filter_node: F, - ) -> FilterCursor<'a, 'b, F, T, U> - where - F: FnMut(&T::Summary) -> bool, - U: Dimension<'a, T::Summary>, - { - FilterCursor::new(self, cx, filter_node) - } - - #[allow(dead_code)] - pub fn first(&self) -> Option<&T> { - self.leftmost_leaf().0.items().first() - } - - pub fn last(&self) -> Option<&T> { - self.rightmost_leaf().0.items().last() - } - - pub fn update_last( - &mut self, - f: impl FnOnce(&mut T), - cx: ::Context<'_>, - ) { - self.update_last_recursive(f, cx); - } - - fn update_last_recursive( - &mut self, - f: impl FnOnce(&mut T), - cx: ::Context<'_>, - ) -> Option { - match Arc::make_mut(&mut self.0) { - Node::Internal { - summary, - child_summaries, - child_trees, - .. - } => { - let last_summary = child_summaries.last_mut().unwrap(); - let last_child = child_trees.last_mut().unwrap(); - *last_summary = last_child.update_last_recursive(f, cx).unwrap(); - *summary = sum(child_summaries.iter(), cx); - Some(summary.clone()) - } - Node::Leaf { - summary, - items, - item_summaries, - } => { - if let Some((item, item_summary)) = items.last_mut().zip(item_summaries.last_mut()) - { - (f)(item); - *item_summary = item.summary(cx); - *summary = sum(item_summaries.iter(), cx); - Some(summary.clone()) - } else { - None - } - } - } - } - - pub fn extent<'a, D: Dimension<'a, T::Summary>>( - &'a self, - cx: ::Context<'_>, - ) -> D { - let mut extent = D::zero(cx); - match self.0.as_ref() { - Node::Internal { summary, .. } | Node::Leaf { summary, .. } => { - extent.add_summary(summary, cx); - } - } - extent - } - - pub fn summary(&self) -> &T::Summary { - match self.0.as_ref() { - Node::Internal { summary, .. } => summary, - Node::Leaf { summary, .. } => summary, - } - } - - pub fn is_empty(&self) -> bool { - match self.0.as_ref() { - Node::Internal { .. } => false, - Node::Leaf { items, .. } => items.is_empty(), - } - } - - pub fn extend(&mut self, iter: I, cx: ::Context<'_>) - where - I: IntoIterator, - { - self.append(Self::from_iter(iter, cx), cx); - } - - pub fn par_extend(&mut self, iter: I, cx: ::Context<'_>) - where - I: IntoParallelIterator, - Iter: IndexedParallelIterator, - T: Send + Sync, - T::Summary: Send + Sync, - for<'a> ::Context<'a>: Sync, - { - self.append(Self::from_par_iter(iter, cx), cx); - } - - pub fn push(&mut self, item: T, cx: ::Context<'_>) { - let summary = item.summary(cx); - self.append( - SumTree(Arc::new(Node::Leaf { - summary: summary.clone(), - items: ArrayVec::from_iter(Some(item)), - item_summaries: ArrayVec::from_iter(Some(summary)), - })), - cx, - ); - } - - pub fn append(&mut self, mut other: Self, cx: ::Context<'_>) { - if self.is_empty() { - *self = other; - } else if !other.0.is_leaf() || !other.0.items().is_empty() { - if self.0.height() < other.0.height() { - if let Some(tree) = Self::append_large(self.clone(), &mut other, cx) { - *self = Self::from_child_trees(tree, other, cx); - } else { - *self = other; - } - } else if let Some(split_tree) = self.push_tree_recursive(other, cx) { - *self = Self::from_child_trees(self.clone(), split_tree, cx); - } - } - } - - fn push_tree_recursive( - &mut self, - other: SumTree, - cx: ::Context<'_>, - ) -> Option> { - match Arc::make_mut(&mut self.0) { - Node::Internal { - height, - summary, - child_summaries, - child_trees, - .. - } => { - let other_node = other.0.clone(); - ::add_summary(summary, other_node.summary(), cx); - - let height_delta = *height - other_node.height(); - let mut summaries_to_append = ArrayVec::::new(); - let mut trees_to_append = ArrayVec::, { 2 * TREE_BASE }>::new(); - if height_delta == 0 { - summaries_to_append.extend(other_node.child_summaries().iter().cloned()); - trees_to_append.extend(other_node.child_trees().iter().cloned()); - } else if height_delta == 1 && !other_node.is_underflowing() { - summaries_to_append.push(other_node.summary().clone()); - trees_to_append.push(other) - } else { - let tree_to_append = child_trees - .last_mut() - .unwrap() - .push_tree_recursive(other, cx); - *child_summaries.last_mut().unwrap() = - child_trees.last().unwrap().0.summary().clone(); - - if let Some(split_tree) = tree_to_append { - summaries_to_append.push(split_tree.0.summary().clone()); - trees_to_append.push(split_tree); - } - } - - let child_count = child_trees.len() + trees_to_append.len(); - if child_count > 2 * TREE_BASE { - let left_summaries: ArrayVec<_, { 2 * TREE_BASE }>; - let right_summaries: ArrayVec<_, { 2 * TREE_BASE }>; - let left_trees; - let right_trees; - - let midpoint = (child_count + child_count % 2) / 2; - { - let mut all_summaries = child_summaries - .iter() - .chain(summaries_to_append.iter()) - .cloned(); - left_summaries = all_summaries.by_ref().take(midpoint).collect(); - right_summaries = all_summaries.collect(); - let mut all_trees = - child_trees.iter().chain(trees_to_append.iter()).cloned(); - left_trees = all_trees.by_ref().take(midpoint).collect(); - right_trees = all_trees.collect(); - } - *summary = sum(left_summaries.iter(), cx); - *child_summaries = left_summaries; - *child_trees = left_trees; - - Some(SumTree(Arc::new(Node::Internal { - height: *height, - summary: sum(right_summaries.iter(), cx), - child_summaries: right_summaries, - child_trees: right_trees, - }))) - } else { - child_summaries.extend(summaries_to_append); - child_trees.extend(trees_to_append); - None - } - } - Node::Leaf { - summary, - items, - item_summaries, - } => { - let other_node = other.0; - - let child_count = items.len() + other_node.items().len(); - if child_count > 2 * TREE_BASE { - let left_items; - let right_items; - let left_summaries; - let right_summaries: ArrayVec; - - let midpoint = (child_count + child_count % 2) / 2; - { - let mut all_items = items.iter().chain(other_node.items().iter()).cloned(); - left_items = all_items.by_ref().take(midpoint).collect(); - right_items = all_items.collect(); - - let mut all_summaries = item_summaries - .iter() - .chain(other_node.child_summaries()) - .cloned(); - left_summaries = all_summaries.by_ref().take(midpoint).collect(); - right_summaries = all_summaries.collect(); - } - *items = left_items; - *item_summaries = left_summaries; - *summary = sum(item_summaries.iter(), cx); - Some(SumTree(Arc::new(Node::Leaf { - items: right_items, - summary: sum(right_summaries.iter(), cx), - item_summaries: right_summaries, - }))) - } else { - ::add_summary(summary, other_node.summary(), cx); - items.extend(other_node.items().iter().cloned()); - item_summaries.extend(other_node.child_summaries().iter().cloned()); - None - } - } - } - } - - // appends the `large` tree to a `small` tree, assumes small.height() <= large.height() - fn append_large( - small: Self, - large: &mut Self, - cx: ::Context<'_>, - ) -> Option { - if small.0.height() == large.0.height() { - if !small.0.is_underflowing() { - Some(small) - } else { - Self::merge_into_right(small, large, cx) - } - } else { - debug_assert!(small.0.height() < large.0.height()); - let Node::Internal { - height, - summary, - child_summaries, - child_trees, - } = Arc::make_mut(&mut large.0) - else { - unreachable!(); - }; - let mut full_summary = small.summary().clone(); - Summary::add_summary(&mut full_summary, summary, cx); - *summary = full_summary; - - let first = child_trees.first_mut().unwrap(); - let res = Self::append_large(small, first, cx); - *child_summaries.first_mut().unwrap() = first.summary().clone(); - if let Some(tree) = res { - if child_trees.len() < 2 * TREE_BASE { - child_summaries.insert(0, tree.summary().clone()); - child_trees.insert(0, tree); - None - } else { - let new_child_summaries = { - let mut res = ArrayVec::from_iter([tree.summary().clone()]); - res.extend(child_summaries.drain(..TREE_BASE)); - res - }; - let tree = SumTree(Arc::new(Node::Internal { - height: *height, - summary: sum(new_child_summaries.iter(), cx), - child_summaries: new_child_summaries, - child_trees: { - let mut res = ArrayVec::from_iter([tree]); - res.extend(child_trees.drain(..TREE_BASE)); - res - }, - })); - - *summary = sum(child_summaries.iter(), cx); - Some(tree) - } - } else { - None - } - } - } - - // Merge two nodes into `large`. - // - // `large` will contain the contents of `small` followed by its own data. - // If the combined data exceed the node capacity, returns a new node that - // holds the first half of the merged items and `large` is left with the - // second half - // - // The nodes must be on the same height - // It only makes sense to call this when `small` is underflowing - fn merge_into_right( - small: Self, - large: &mut Self, - cx: <::Summary as Summary>::Context<'_>, - ) -> Option> { - debug_assert_eq!(small.0.height(), large.0.height()); - match (small.0.as_ref(), Arc::make_mut(&mut large.0)) { - ( - Node::Internal { - summary: small_summary, - child_summaries: small_child_summaries, - child_trees: small_child_trees, - .. - }, - Node::Internal { - summary, - child_summaries, - child_trees, - height, - }, - ) => { - let total_child_count = child_trees.len() + small_child_trees.len(); - if total_child_count <= 2 * TREE_BASE { - let mut all_trees = small_child_trees.clone(); - all_trees.extend(child_trees.drain(..)); - *child_trees = all_trees; - - let mut all_summaries = small_child_summaries.clone(); - all_summaries.extend(child_summaries.drain(..)); - *child_summaries = all_summaries; - - let mut full_summary = small_summary.clone(); - Summary::add_summary(&mut full_summary, summary, cx); - *summary = full_summary; - None - } else { - let midpoint = total_child_count.div_ceil(2); - let mut all_trees = small_child_trees.iter().chain(child_trees.iter()).cloned(); - let left_trees = all_trees.by_ref().take(midpoint).collect(); - *child_trees = all_trees.collect(); - - let mut all_summaries = small_child_summaries - .iter() - .chain(child_summaries.iter()) - .cloned(); - let left_summaries: ArrayVec<_, { 2 * TREE_BASE }> = - all_summaries.by_ref().take(midpoint).collect(); - *child_summaries = all_summaries.collect(); - - *summary = sum(child_summaries.iter(), cx); - Some(SumTree(Arc::new(Node::Internal { - height: *height, - summary: sum(left_summaries.iter(), cx), - child_summaries: left_summaries, - child_trees: left_trees, - }))) - } - } - ( - Node::Leaf { - summary: small_summary, - items: small_items, - item_summaries: small_item_summaries, - }, - Node::Leaf { - summary, - items, - item_summaries, - }, - ) => { - let total_child_count = small_items.len() + items.len(); - if total_child_count <= 2 * TREE_BASE { - let mut all_items = small_items.clone(); - all_items.extend(items.drain(..)); - *items = all_items; - - let mut all_summaries = small_item_summaries.clone(); - all_summaries.extend(item_summaries.drain(..)); - *item_summaries = all_summaries; - - let mut full_summary = small_summary.clone(); - Summary::add_summary(&mut full_summary, summary, cx); - *summary = full_summary; - None - } else { - let midpoint = total_child_count.div_ceil(2); - let mut all_items = small_items.iter().chain(items.iter()).cloned(); - let left_items = all_items.by_ref().take(midpoint).collect(); - *items = all_items.collect(); - - let mut all_summaries = small_item_summaries - .iter() - .chain(item_summaries.iter()) - .cloned(); - let left_summaries: ArrayVec<_, { 2 * TREE_BASE }> = - all_summaries.by_ref().take(midpoint).collect(); - *item_summaries = all_summaries.collect(); - - *summary = sum(item_summaries.iter(), cx); - Some(SumTree(Arc::new(Node::Leaf { - items: left_items, - summary: sum(left_summaries.iter(), cx), - item_summaries: left_summaries, - }))) - } - } - _ => unreachable!(), - } - } - - fn from_child_trees( - left: SumTree, - right: SumTree, - cx: ::Context<'_>, - ) -> Self { - let height = left.0.height() + 1; - let mut child_summaries = ArrayVec::new(); - child_summaries.push(left.0.summary().clone()); - child_summaries.push(right.0.summary().clone()); - let mut child_trees = ArrayVec::new(); - child_trees.push(left); - child_trees.push(right); - SumTree(Arc::new(Node::Internal { - height, - summary: sum(child_summaries.iter(), cx), - child_summaries, - child_trees, - })) - } - - fn leftmost_leaf(&self) -> &Self { - match *self.0 { - Node::Leaf { .. } => self, - Node::Internal { - ref child_trees, .. - } => child_trees.first().unwrap().leftmost_leaf(), - } - } - - fn rightmost_leaf(&self) -> &Self { - match *self.0 { - Node::Leaf { .. } => self, - Node::Internal { - ref child_trees, .. - } => child_trees.last().unwrap().rightmost_leaf(), - } - } -} - -impl PartialEq for SumTree { - fn eq(&self, other: &Self) -> bool { - self.iter().eq(other.iter()) - } -} - -impl Eq for SumTree {} - -impl SumTree { - pub fn insert_or_replace<'a, 'b>( - &'a mut self, - item: T, - cx: ::Context<'b>, - ) -> Option { - let mut replaced = None; - { - let mut cursor = self.cursor::(cx); - let mut new_tree = cursor.slice(&item.key(), Bias::Left); - if let Some(cursor_item) = cursor.item() - && cursor_item.key() == item.key() - { - replaced = Some(cursor_item.clone()); - cursor.next(); - } - new_tree.push(item, cx); - new_tree.append(cursor.suffix(), cx); - drop(cursor); - *self = new_tree - }; - replaced - } - - pub fn remove(&mut self, key: &T::Key, cx: ::Context<'_>) -> Option { - let mut removed = None; - *self = { - let mut cursor = self.cursor::(cx); - let mut new_tree = cursor.slice(key, Bias::Left); - if let Some(item) = cursor.item() - && item.key() == *key - { - removed = Some(item.clone()); - cursor.next(); - } - new_tree.append(cursor.suffix(), cx); - new_tree - }; - removed - } - - pub fn edit( - &mut self, - mut edits: Vec>, - cx: ::Context<'_>, - ) -> Vec { - if edits.is_empty() { - return Vec::new(); - } - - let mut removed = Vec::new(); - edits.sort_unstable_by_key(|item| item.key()); - - *self = { - let mut cursor = self.cursor::(cx); - let mut new_tree = SumTree::new(cx); - let mut buffered_items = Vec::new(); - - cursor.seek(&T::Key::zero(cx), Bias::Left); - for edit in edits { - let new_key = edit.key(); - let mut old_item = cursor.item(); - - if old_item - .as_ref() - .is_some_and(|old_item| old_item.key() < new_key) - { - new_tree.extend(buffered_items.drain(..), cx); - let slice = cursor.slice(&new_key, Bias::Left); - new_tree.append(slice, cx); - old_item = cursor.item(); - } - - if let Some(old_item) = old_item - && old_item.key() == new_key - { - removed.push(old_item.clone()); - cursor.next(); - } - - match edit { - Edit::Insert(item) => { - buffered_items.push(item); - } - Edit::Remove(_) => {} - } - } - - new_tree.extend(buffered_items, cx); - new_tree.append(cursor.suffix(), cx); - new_tree - }; - - removed - } - - pub fn get<'a>( - &'a self, - key: &T::Key, - cx: ::Context<'a>, - ) -> Option<&'a T> { - if let (_, _, Some(item)) = self.find_exact::(cx, key, Bias::Left) { - Some(item) - } else { - None - } - } -} - -impl Default for SumTree -where - T: Item, - S: for<'a> Summary = ()>, -{ - fn default() -> Self { - Self::new(()) - } -} - -#[derive(Clone)] -pub enum Node { - Internal { - height: u8, - summary: T::Summary, - child_summaries: ArrayVec, - child_trees: ArrayVec, { 2 * TREE_BASE }>, - }, - Leaf { - summary: T::Summary, - items: ArrayVec, - item_summaries: ArrayVec, - }, -} - -impl fmt::Debug for Node -where - T: Item + fmt::Debug, - T::Summary: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Node::Internal { - height, - summary, - child_summaries, - child_trees, - } => f - .debug_struct("Internal") - .field("height", height) - .field("summary", summary) - .field("child_summaries", child_summaries) - .field("child_trees", child_trees) - .finish(), - Node::Leaf { - summary, - items, - item_summaries, - } => f - .debug_struct("Leaf") - .field("summary", summary) - .field("items", items) - .field("item_summaries", item_summaries) - .finish(), - } - } -} - -impl Node { - fn is_leaf(&self) -> bool { - matches!(self, Node::Leaf { .. }) - } - - fn height(&self) -> u8 { - match self { - Node::Internal { height, .. } => *height, - Node::Leaf { .. } => 0, - } - } - - fn summary(&self) -> &T::Summary { - match self { - Node::Internal { summary, .. } => summary, - Node::Leaf { summary, .. } => summary, - } - } - - fn child_summaries(&self) -> &[T::Summary] { - match self { - Node::Internal { - child_summaries, .. - } => child_summaries.as_slice(), - Node::Leaf { item_summaries, .. } => item_summaries.as_slice(), - } - } - - fn child_trees(&self) -> &ArrayVec, { 2 * TREE_BASE }> { - match self { - Node::Internal { child_trees, .. } => child_trees, - Node::Leaf { .. } => panic!("Leaf nodes have no child trees"), - } - } - - fn items(&self) -> &ArrayVec { - match self { - Node::Leaf { items, .. } => items, - Node::Internal { .. } => panic!("Internal nodes have no items"), - } - } - - fn is_underflowing(&self) -> bool { - match self { - Node::Internal { child_trees, .. } => child_trees.len() < TREE_BASE, - Node::Leaf { items, .. } => items.len() < TREE_BASE, - } - } -} - -#[derive(Debug)] -pub enum Edit { - Insert(T), - Remove(T::Key), -} - -impl Edit { - fn key(&self) -> T::Key { - match self { - Edit::Insert(item) => item.key(), - Edit::Remove(key) => key.clone(), - } - } -} - -fn sum<'a, T, I>(iter: I, cx: T::Context<'_>) -> T -where - T: 'a + Summary, - I: Iterator, -{ - let mut sum = T::zero(cx); - for value in iter { - sum.add_summary(value, cx); - } - sum -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::{distr::StandardUniform, prelude::*}; - use std::cmp; - - #[test] - fn test_extend_and_push_tree() { - let mut tree1 = SumTree::default(); - tree1.extend(0..20, ()); - - let mut tree2 = SumTree::default(); - tree2.extend(50..100, ()); - - tree1.append(tree2, ()); - assert_eq!(tree1.items(()), (0..20).chain(50..100).collect::>()); - } - - #[test] - fn test_random() { - let mut starting_seed = 0; - if let Ok(value) = std::env::var("SEED") { - starting_seed = value.parse().expect("invalid SEED variable"); - } - let mut num_iterations = 100; - if let Ok(value) = std::env::var("ITERATIONS") { - num_iterations = value.parse().expect("invalid ITERATIONS variable"); - } - let num_operations = std::env::var("OPERATIONS") - .map_or(5, |o| o.parse().expect("invalid OPERATIONS variable")); - - for seed in starting_seed..(starting_seed + num_iterations) { - eprintln!("seed = {}", seed); - let mut rng = StdRng::seed_from_u64(seed); - - let rng = &mut rng; - let mut tree = SumTree::::default(); - let count = rng.random_range(0..10); - if rng.random() { - tree.extend(rng.sample_iter(StandardUniform).take(count), ()); - } else { - let items = rng - .sample_iter(StandardUniform) - .take(count) - .collect::>(); - tree.par_extend(items, ()); - } - - for _ in 0..num_operations { - let splice_end = rng.random_range(0..tree.extent::(()).0 + 1); - let splice_start = rng.random_range(0..splice_end + 1); - let count = rng.random_range(0..10); - let tree_end = tree.extent::(()); - let new_items = rng - .sample_iter(StandardUniform) - .take(count) - .collect::>(); - - let mut reference_items = tree.items(()); - reference_items.splice(splice_start..splice_end, new_items.clone()); - - tree = { - let mut cursor = tree.cursor::(()); - let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right); - if rng.random() { - new_tree.extend(new_items, ()); - } else { - new_tree.par_extend(new_items, ()); - } - cursor.seek(&Count(splice_end), Bias::Right); - new_tree.append(cursor.slice(&tree_end, Bias::Right), ()); - new_tree - }; - - assert_eq!(tree.items(()), reference_items); - assert_eq!( - tree.iter().collect::>(), - tree.cursor::<()>(()).collect::>() - ); - - log::info!("tree items: {:?}", tree.items(())); - - let mut filter_cursor = - tree.filter::<_, Count>((), |summary| summary.contains_even); - let expected_filtered_items = tree - .items(()) - .into_iter() - .enumerate() - .filter(|(_, item)| (item & 1) == 0) - .collect::>(); - - let mut item_ix = if rng.random() { - filter_cursor.next(); - 0 - } else { - filter_cursor.prev(); - expected_filtered_items.len().saturating_sub(1) - }; - while item_ix < expected_filtered_items.len() { - log::info!("filter_cursor, item_ix: {}", item_ix); - let actual_item = filter_cursor.item().unwrap(); - let (reference_index, reference_item) = expected_filtered_items[item_ix]; - assert_eq!(actual_item, &reference_item); - assert_eq!(filter_cursor.start().0, reference_index); - log::info!("next"); - filter_cursor.next(); - item_ix += 1; - - while item_ix > 0 && rng.random_bool(0.2) { - log::info!("prev"); - filter_cursor.prev(); - item_ix -= 1; - - if item_ix == 0 && rng.random_bool(0.2) { - filter_cursor.prev(); - assert_eq!(filter_cursor.item(), None); - assert_eq!(filter_cursor.start().0, 0); - filter_cursor.next(); - } - } - } - assert_eq!(filter_cursor.item(), None); - - let mut before_start = false; - let mut cursor = tree.cursor::(()); - let start_pos = rng.random_range(0..=reference_items.len()); - cursor.seek(&Count(start_pos), Bias::Right); - let mut pos = rng.random_range(start_pos..=reference_items.len()); - cursor.seek_forward(&Count(pos), Bias::Right); - - for i in 0..10 { - assert_eq!(cursor.start().0, pos); - - if pos > 0 { - assert_eq!(cursor.prev_item().unwrap(), &reference_items[pos - 1]); - } else { - assert_eq!(cursor.prev_item(), None); - } - - if pos < reference_items.len() && !before_start { - assert_eq!(cursor.item().unwrap(), &reference_items[pos]); - } else { - assert_eq!(cursor.item(), None); - } - - if before_start { - assert_eq!(cursor.next_item(), reference_items.first()); - } 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() { - pos += 1; - before_start = false; - } - } else { - cursor.prev(); - if pos == 0 { - before_start = true; - } - pos = pos.saturating_sub(1); - } - } - } - - for _ in 0..10 { - let end = rng.random_range(0..tree.extent::(()).0 + 1); - let start = rng.random_range(0..end + 1); - let start_bias = if rng.random() { - Bias::Left - } else { - Bias::Right - }; - let end_bias = if rng.random() { - Bias::Left - } else { - Bias::Right - }; - - let mut cursor = tree.cursor::(()); - cursor.seek(&Count(start), start_bias); - let slice = cursor.slice(&Count(end), end_bias); - - cursor.seek(&Count(start), start_bias); - let summary = cursor.summary::<_, Sum>(&Count(end), end_bias); - - assert_eq!(summary.0, slice.summary().sum); - } - } - } - - #[test] - fn test_cursor() { - // Empty tree - let tree = SumTree::::default(); - let mut cursor = tree.cursor::(()); - assert_eq!( - cursor.slice(&Count(0), Bias::Right).items(()), - Vec::::new() - ); - 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 - let mut tree = SumTree::::default(); - tree.extend(vec![1], ()); - let mut cursor = tree.cursor::(()); - assert_eq!( - cursor.slice(&Count(0), Bias::Right).items(()), - Vec::::new() - ); - 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); - assert_eq!( - cursor - .slice(&tree.extent::(()), 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); - - // Multiple-element tree - let mut tree = SumTree::default(); - tree.extend(vec![1, 2, 3, 4, 5, 6], ()); - let mut cursor = tree.cursor::(()); - - 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::(()); - assert_eq!( - cursor - .slice(&tree.extent::(()), Bias::Right) - .items(()), - tree.items(()) - ); - 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); - assert_eq!( - cursor - .slice(&tree.extent::(()), Bias::Right) - .items(()), - [4, 5, 6] - ); - 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 - cursor.seek(&Count(1), Bias::Left); - assert_eq!(cursor.item(), Some(&1)); - cursor.seek(&Count(1), Bias::Right); - assert_eq!(cursor.item(), Some(&2)); - - // Slicing without resetting starts from where the cursor is parked at. - cursor.seek(&Count(1), Bias::Right); - assert_eq!(cursor.slice(&Count(3), Bias::Right).items(()), vec![2, 3]); - assert_eq!(cursor.slice(&Count(6), Bias::Left).items(()), vec![4, 5]); - assert_eq!(cursor.slice(&Count(6), Bias::Right).items(()), vec![6]); - } - - #[test] - fn test_edit() { - let mut tree = SumTree::::default(); - - let removed = tree.edit(vec![Edit::Insert(1), Edit::Insert(2), Edit::Insert(0)], ()); - assert_eq!(tree.items(()), vec![0, 1, 2]); - assert_eq!(removed, Vec::::new()); - assert_eq!(tree.get(&0, ()), Some(&0)); - assert_eq!(tree.get(&1, ()), Some(&1)); - assert_eq!(tree.get(&2, ()), Some(&2)); - assert_eq!(tree.get(&4, ()), None); - - let removed = tree.edit(vec![Edit::Insert(2), Edit::Insert(4), Edit::Remove(0)], ()); - assert_eq!(tree.items(()), vec![1, 2, 4]); - assert_eq!(removed, vec![0, 2]); - assert_eq!(tree.get(&0, ()), None); - assert_eq!(tree.get(&1, ()), Some(&1)); - assert_eq!(tree.get(&2, ()), Some(&2)); - assert_eq!(tree.get(&4, ()), Some(&4)); - } - - #[test] - fn test_from_iter() { - assert_eq!( - SumTree::from_iter(0..100, ()).items(()), - (0..100).collect::>() - ); - - // Ensure `from_iter` works correctly when the given iterator restarts - // after calling `next` if `None` was already returned. - let mut ix = 0; - let iterator = std::iter::from_fn(|| { - ix = (ix + 1) % 2; - if ix == 1 { Some(1) } else { None } - }); - assert_eq!(SumTree::from_iter(iterator, ()).items(()), vec![1]); - } - - #[derive(Clone, Default, Debug)] - pub struct IntegersSummary { - count: usize, - sum: usize, - contains_even: bool, - max: u8, - } - - #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] - struct Count(usize); - - #[derive(Ord, PartialOrd, Default, Eq, PartialEq, Clone, Debug)] - struct Sum(usize); - - impl Item for u8 { - type Summary = IntegersSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - IntegersSummary { - count: 1, - sum: *self as usize, - contains_even: (*self & 1) == 0, - max: *self, - } - } - } - - impl KeyedItem for u8 { - type Key = u8; - - fn key(&self) -> Self::Key { - *self - } - } - - impl ContextLessSummary for IntegersSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, other: &Self) { - self.count += other.count; - self.sum += other.sum; - self.contains_even |= other.contains_even; - self.max = cmp::max(self.max, other.max); - } - } - - impl Dimension<'_, IntegersSummary> for u8 { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { - *self = summary.max; - } - } - - impl Dimension<'_, IntegersSummary> for Count { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { - self.0 += summary.count; - } - } - - impl SeekTarget<'_, IntegersSummary, IntegersSummary> for Count { - fn cmp(&self, cursor_location: &IntegersSummary, _: ()) -> Ordering { - self.0.cmp(&cursor_location.count) - } - } - - impl Dimension<'_, IntegersSummary> for Sum { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &IntegersSummary, _: ()) { - self.0 += summary.sum; - } - } -} diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs deleted file mode 100644 index e58f7a65dd..0000000000 --- a/crates/sum_tree/src/tree_map.rs +++ /dev/null @@ -1,527 +0,0 @@ -use std::{cmp::Ordering, fmt::Debug}; - -use crate::{Bias, ContextLessSummary, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree}; - -/// A cheaply-cloneable ordered map based on a [SumTree](crate::SumTree). -#[derive(Clone, PartialEq, Eq)] -pub struct TreeMap(SumTree>) -where - K: Clone + Ord, - V: Clone; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MapEntry { - key: K, - value: V, -} - -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct MapKey(Option); - -impl Default for MapKey { - fn default() -> Self { - Self(None) - } -} - -#[derive(Clone, Debug)] -pub struct MapKeyRef<'a, K>(Option<&'a K>); - -impl Default for MapKeyRef<'_, K> { - fn default() -> Self { - Self(None) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct TreeSet(TreeMap) -where - K: Clone + Ord; - -impl TreeMap { - pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { - let tree = SumTree::from_iter( - entries - .into_iter() - .map(|(key, value)| MapEntry { key, value }), - (), - ); - Self(tree) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn get(&self, key: &K) -> Option<&V> { - let (.., item) = self - .0 - .find::, _>((), &MapKeyRef(Some(key)), Bias::Left); - if let Some(item) = item { - if Some(key) == item.key().0.as_ref() { - Some(&item.value) - } else { - None - } - } else { - None - } - } - - pub fn insert(&mut self, key: K, value: V) { - self.0.insert_or_replace(MapEntry { key, value }, ()); - } - - pub fn insert_or_replace(&mut self, key: K, value: V) -> Option { - self.0 - .insert_or_replace(MapEntry { key, value }, ()) - .map(|it| it.value) - } - - pub fn extend(&mut self, iter: impl IntoIterator) { - let edits: Vec<_> = iter - .into_iter() - .map(|(key, value)| Edit::Insert(MapEntry { key, value })) - .collect(); - self.0.edit(edits, ()); - } - - pub fn clear(&mut self) { - self.0 = SumTree::default(); - } - - pub fn remove(&mut self, key: &K) -> Option { - let mut removed = None; - let mut cursor = self.0.cursor::>(()); - let key = MapKeyRef(Some(key)); - let mut new_tree = cursor.slice(&key, Bias::Left); - if key.cmp(&cursor.end(), ()) == Ordering::Equal { - removed = Some(cursor.item().unwrap().value.clone()); - cursor.next(); - } - new_tree.append(cursor.suffix(), ()); - drop(cursor); - self.0 = new_tree; - removed - } - - pub fn remove_range(&mut self, start: &impl MapSeekTarget, end: &impl MapSeekTarget) { - let start = MapSeekTargetAdaptor(start); - let end = MapSeekTargetAdaptor(end); - let mut cursor = self.0.cursor::>(()); - let mut new_tree = cursor.slice(&start, Bias::Left); - cursor.seek(&end, Bias::Left); - new_tree.append(cursor.suffix(), ()); - drop(cursor); - self.0 = new_tree; - } - - /// Returns the key-value pair with the greatest key less than or equal to the given key. - pub fn closest(&self, key: &K) -> Option<(&K, &V)> { - let mut cursor = self.0.cursor::>(()); - let key = MapKeyRef(Some(key)); - cursor.seek(&key, Bias::Right); - cursor.prev(); - cursor.item().map(|item| (&item.key, &item.value)) - } - - pub fn iter_from<'a>(&'a self, from: &K) -> impl Iterator + 'a { - let mut cursor = self.0.cursor::>(()); - let from_key = MapKeyRef(Some(from)); - cursor.seek(&from_key, Bias::Left); - - cursor.map(|map_entry| (&map_entry.key, &map_entry.value)) - } - - pub fn update(&mut self, key: &K, f: F) -> Option - where - F: FnOnce(&mut V) -> T, - { - let mut cursor = self.0.cursor::>(()); - let key = MapKeyRef(Some(key)); - let mut new_tree = cursor.slice(&key, Bias::Left); - let mut result = None; - if key.cmp(&cursor.end(), ()) == Ordering::Equal { - let mut updated = cursor.item().unwrap().clone(); - result = Some(f(&mut updated.value)); - new_tree.push(updated, ()); - cursor.next(); - } - new_tree.append(cursor.suffix(), ()); - drop(cursor); - self.0 = new_tree; - result - } - - pub fn retain bool>(&mut self, mut predicate: F) { - let mut new_map = SumTree::>::default(); - - let mut cursor = self.0.cursor::>(()); - cursor.next(); - while let Some(item) = cursor.item() { - if predicate(&item.key, &item.value) { - new_map.push(item.clone(), ()); - } - cursor.next(); - } - drop(cursor); - - self.0 = new_map; - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().map(|entry| (&entry.key, &entry.value)) - } - - pub fn values(&self) -> impl Iterator + '_ { - self.0.iter().map(|entry| &entry.value) - } - - pub fn first(&self) -> Option<(&K, &V)> { - self.0.first().map(|entry| (&entry.key, &entry.value)) - } - - pub fn last(&self) -> Option<(&K, &V)> { - self.0.last().map(|entry| (&entry.key, &entry.value)) - } - - pub fn insert_tree(&mut self, other: TreeMap) { - let edits = other - .iter() - .map(|(key, value)| { - Edit::Insert(MapEntry { - key: key.to_owned(), - value: value.to_owned(), - }) - }) - .collect(); - - self.0.edit(edits, ()); - } -} - -impl Debug for TreeMap -where - K: Clone + Debug + Ord, - V: Clone + Debug, -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_map().entries(self.iter()).finish() - } -} - -#[derive(Debug)] -struct MapSeekTargetAdaptor<'a, T>(&'a T); - -impl<'a, K: Clone + Ord, T: MapSeekTarget> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> - for MapSeekTargetAdaptor<'_, T> -{ - fn cmp(&self, cursor_location: &MapKeyRef, _: ()) -> Ordering { - if let Some(key) = &cursor_location.0 { - MapSeekTarget::cmp_cursor(self.0, key) - } else { - Ordering::Greater - } - } -} - -pub trait MapSeekTarget { - fn cmp_cursor(&self, cursor_location: &K) -> Ordering; -} - -impl MapSeekTarget for K { - fn cmp_cursor(&self, cursor_location: &K) -> Ordering { - self.cmp(cursor_location) - } -} - -impl Default for TreeMap -where - K: Clone + Ord, - V: Clone, -{ - fn default() -> Self { - Self(Default::default()) - } -} - -impl Item for MapEntry -where - K: Clone + Ord, - V: Clone, -{ - type Summary = MapKey; - - fn summary(&self, _cx: ()) -> Self::Summary { - self.key() - } -} - -impl KeyedItem for MapEntry -where - K: Clone + Ord, - V: Clone, -{ - type Key = MapKey; - - fn key(&self) -> Self::Key { - MapKey(Some(self.key.clone())) - } -} - -impl ContextLessSummary for MapKey -where - K: Clone, -{ - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - *self = summary.clone() - } -} - -impl<'a, K> Dimension<'a, MapKey> for MapKeyRef<'a, K> -where - K: Clone + Ord, -{ - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a MapKey, _: ()) { - self.0 = summary.0.as_ref(); - } -} - -impl<'a, K> SeekTarget<'a, MapKey, MapKeyRef<'a, K>> for MapKeyRef<'_, K> -where - K: Clone + Ord, -{ - fn cmp(&self, cursor_location: &MapKeyRef, _: ()) -> Ordering { - Ord::cmp(&self.0, &cursor_location.0) - } -} - -impl Default for TreeSet -where - K: Clone + Ord, -{ - fn default() -> Self { - Self(Default::default()) - } -} - -impl TreeSet -where - K: Clone + Ord, -{ - pub fn from_ordered_entries(entries: impl IntoIterator) -> Self { - Self(TreeMap::from_ordered_entries( - entries.into_iter().map(|key| (key, ())), - )) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn insert(&mut self, key: K) { - self.0.insert(key, ()); - } - - pub fn remove(&mut self, key: &K) -> bool { - self.0.remove(key).is_some() - } - - pub fn extend(&mut self, iter: impl IntoIterator) { - self.0.extend(iter.into_iter().map(|key| (key, ()))); - } - - pub fn contains(&self, key: &K) -> bool { - self.0.get(key).is_some() - } - - pub fn iter(&self) -> impl Iterator + '_ { - self.0.iter().map(|(k, _)| k) - } - - pub fn iter_from<'a>(&'a self, key: &K) -> impl Iterator + 'a { - self.0.iter_from(key).map(move |(k, _)| k) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_basic() { - let mut map = TreeMap::default(); - assert_eq!(map.iter().collect::>(), vec![]); - - map.insert(3, "c"); - assert_eq!(map.get(&3), Some(&"c")); - assert_eq!(map.iter().collect::>(), vec![(&3, &"c")]); - - map.insert(1, "a"); - assert_eq!(map.get(&1), Some(&"a")); - assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); - - map.insert(2, "b"); - assert_eq!(map.get(&2), Some(&"b")); - assert_eq!(map.get(&1), Some(&"a")); - assert_eq!(map.get(&3), Some(&"c")); - assert_eq!( - map.iter().collect::>(), - vec![(&1, &"a"), (&2, &"b"), (&3, &"c")] - ); - - assert_eq!(map.closest(&0), None); - assert_eq!(map.closest(&1), Some((&1, &"a"))); - assert_eq!(map.closest(&10), Some((&3, &"c"))); - - map.remove(&2); - assert_eq!(map.get(&2), None); - assert_eq!(map.iter().collect::>(), vec![(&1, &"a"), (&3, &"c")]); - - assert_eq!(map.closest(&2), Some((&1, &"a"))); - - map.remove(&3); - assert_eq!(map.get(&3), None); - assert_eq!(map.iter().collect::>(), vec![(&1, &"a")]); - - map.remove(&1); - assert_eq!(map.get(&1), None); - assert_eq!(map.iter().collect::>(), vec![]); - - map.insert(4, "d"); - map.insert(5, "e"); - map.insert(6, "f"); - map.retain(|key, _| *key % 2 == 0); - assert_eq!(map.iter().collect::>(), vec![(&4, &"d"), (&6, &"f")]); - } - - #[test] - fn test_iter_from() { - let mut map = TreeMap::default(); - - map.insert("a", 1); - map.insert("b", 2); - map.insert("baa", 3); - map.insert("baaab", 4); - map.insert("c", 5); - - let result = map - .iter_from(&"ba") - .take_while(|(key, _)| key.starts_with("ba")) - .collect::>(); - - assert_eq!(result.len(), 2); - assert!(result.iter().any(|(k, _)| k == &&"baa")); - assert!(result.iter().any(|(k, _)| k == &&"baaab")); - - let result = map - .iter_from(&"c") - .take_while(|(key, _)| key.starts_with("c")) - .collect::>(); - - assert_eq!(result.len(), 1); - assert!(result.iter().any(|(k, _)| k == &&"c")); - } - - #[test] - fn test_insert_tree() { - let mut map = TreeMap::default(); - map.insert("a", 1); - map.insert("b", 2); - map.insert("c", 3); - - let mut other = TreeMap::default(); - other.insert("a", 2); - other.insert("b", 2); - other.insert("d", 4); - - map.insert_tree(other); - - assert_eq!(map.iter().count(), 4); - assert_eq!(map.get(&"a"), Some(&2)); - assert_eq!(map.get(&"b"), Some(&2)); - assert_eq!(map.get(&"c"), Some(&3)); - assert_eq!(map.get(&"d"), Some(&4)); - } - - #[test] - fn test_extend() { - let mut map = TreeMap::default(); - map.insert("a", 1); - map.insert("b", 2); - map.insert("c", 3); - map.extend([("a", 2), ("b", 2), ("d", 4)]); - assert_eq!(map.iter().count(), 4); - assert_eq!(map.get(&"a"), Some(&2)); - assert_eq!(map.get(&"b"), Some(&2)); - assert_eq!(map.get(&"c"), Some(&3)); - assert_eq!(map.get(&"d"), Some(&4)); - } - - #[test] - fn test_remove_between_and_path_successor() { - use std::path::{Path, PathBuf}; - - #[derive(Debug)] - pub struct PathDescendants<'a>(&'a Path); - - impl MapSeekTarget for PathDescendants<'_> { - fn cmp_cursor(&self, key: &PathBuf) -> Ordering { - if key.starts_with(self.0) { - Ordering::Greater - } else { - self.0.cmp(key) - } - } - } - - let mut map = TreeMap::default(); - - map.insert(PathBuf::from("a"), 1); - map.insert(PathBuf::from("a/a"), 1); - map.insert(PathBuf::from("b"), 2); - map.insert(PathBuf::from("b/a/a"), 3); - map.insert(PathBuf::from("b/a/a/a/b"), 4); - map.insert(PathBuf::from("c"), 5); - map.insert(PathBuf::from("c/a"), 6); - - map.remove_range( - &PathBuf::from("b/a"), - &PathDescendants(&PathBuf::from("b/a")), - ); - - assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); - assert_eq!(map.get(&PathBuf::from("b/a/a")), None); - assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None); - assert_eq!(map.get(&PathBuf::from("c")), Some(&5)); - assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6)); - - map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c"))); - - assert_eq!(map.get(&PathBuf::from("a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1)); - assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); - assert_eq!(map.get(&PathBuf::from("c")), None); - assert_eq!(map.get(&PathBuf::from("c/a")), None); - - map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a"))); - - assert_eq!(map.get(&PathBuf::from("a")), None); - assert_eq!(map.get(&PathBuf::from("a/a")), None); - assert_eq!(map.get(&PathBuf::from("b")), Some(&2)); - - map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b"))); - - assert_eq!(map.get(&PathBuf::from("b")), None); - } -} diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml deleted file mode 100644 index f58bc32bb4..0000000000 --- a/crates/util/Cargo.toml +++ /dev/null @@ -1,65 +0,0 @@ -[package] -name = "util" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "A collection of utility structs and functions used by Zed and GPUI" - - -[lints] -workspace = true - -[lib] -path = "src/util.rs" -doctest = true - -[features] -test-support = ["git2", "rand", "util_macros"] - -[dependencies] -anyhow.workspace = true -async-fs.workspace = true -async_zip.workspace = true -collections.workspace = true -dirs.workspace = true -dunce = "1.0" -futures-lite.workspace = true -futures.workspace = true -git2 = { workspace = true, optional = true } -globset.workspace = true -itertools.workspace = true -log.workspace = true -rand = { workspace = true, optional = true } -regex.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -shlex.workspace = true -smol.workspace = true -take-until.workspace = true -tempfile.workspace = true -unicase.workspace = true -util_macros = { workspace = true, optional = true } -walkdir.workspace = true -which.workspace = true - -[target.'cfg(unix)'.dependencies] -command-fds = "0.3.1" -libc.workspace = true -nix = { workspace = true, features = ["user"] } - -[target.'cfg(target_os = "macos")'.dependencies] -mach2.workspace = true - -[target.'cfg(windows)'.dependencies] -tendril = "0.4.3" - -[dev-dependencies] -git2.workspace = true -indoc.workspace = true -rand.workspace = true -util_macros.workspace = true -pretty_assertions.workspace = true diff --git a/crates/util/LICENSE-APACHE b/crates/util/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/util/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/util/src/arc_cow.rs b/crates/util/src/arc_cow.rs deleted file mode 100644 index 41040671bc..0000000000 --- a/crates/util/src/arc_cow.rs +++ /dev/null @@ -1,141 +0,0 @@ -use std::{ - borrow::Cow, - cmp::Ordering, - fmt::{self, Debug}, - hash::{Hash, Hasher}, - sync::Arc, -}; - -pub enum ArcCow<'a, T: ?Sized> { - Borrowed(&'a T), - Owned(Arc), -} - -impl PartialEq for ArcCow<'_, T> { - fn eq(&self, other: &Self) -> bool { - let a = self.as_ref(); - let b = other.as_ref(); - a == b - } -} - -impl PartialOrd for ArcCow<'_, T> { - fn partial_cmp(&self, other: &Self) -> Option { - self.as_ref().partial_cmp(other.as_ref()) - } -} - -impl Ord for ArcCow<'_, T> { - fn cmp(&self, other: &Self) -> Ordering { - self.as_ref().cmp(other.as_ref()) - } -} - -impl Eq for ArcCow<'_, T> {} - -impl Hash for ArcCow<'_, T> { - fn hash(&self, state: &mut H) { - match self { - Self::Borrowed(borrowed) => Hash::hash(borrowed, state), - Self::Owned(owned) => Hash::hash(&**owned, state), - } - } -} - -impl Clone for ArcCow<'_, T> { - fn clone(&self) -> Self { - match self { - Self::Borrowed(borrowed) => Self::Borrowed(borrowed), - Self::Owned(owned) => Self::Owned(owned.clone()), - } - } -} - -impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> { - fn from(s: &'a T) -> Self { - Self::Borrowed(s) - } -} - -impl From> for ArcCow<'_, T> { - fn from(s: Arc) -> Self { - Self::Owned(s) - } -} - -impl From<&'_ Arc> for ArcCow<'_, T> { - fn from(s: &'_ Arc) -> Self { - Self::Owned(s.clone()) - } -} - -impl From for ArcCow<'_, str> { - fn from(value: String) -> Self { - Self::Owned(value.into()) - } -} - -impl From<&String> for ArcCow<'_, str> { - fn from(value: &String) -> Self { - Self::Owned(value.clone().into()) - } -} - -impl<'a> From> for ArcCow<'a, str> { - fn from(value: Cow<'a, str>) -> Self { - match value { - Cow::Borrowed(borrowed) => Self::Borrowed(borrowed), - Cow::Owned(owned) => Self::Owned(owned.into()), - } - } -} - -impl From> for ArcCow<'_, [T]> { - fn from(vec: Vec) -> Self { - ArcCow::Owned(Arc::from(vec)) - } -} - -impl<'a> From<&'a str> for ArcCow<'a, [u8]> { - fn from(s: &'a str) -> Self { - ArcCow::Borrowed(s.as_bytes()) - } -} - -impl std::borrow::Borrow for ArcCow<'_, T> { - fn borrow(&self) -> &T { - match self { - ArcCow::Borrowed(borrowed) => borrowed, - ArcCow::Owned(owned) => owned.as_ref(), - } - } -} - -impl std::ops::Deref for ArcCow<'_, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - match self { - ArcCow::Borrowed(s) => s, - ArcCow::Owned(s) => s.as_ref(), - } - } -} - -impl AsRef for ArcCow<'_, T> { - fn as_ref(&self) -> &T { - match self { - ArcCow::Borrowed(borrowed) => borrowed, - ArcCow::Owned(owned) => owned.as_ref(), - } - } -} - -impl Debug for ArcCow<'_, T> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f), - ArcCow::Owned(owned) => Debug::fmt(&**owned, f), - } - } -} diff --git a/crates/util/src/archive.rs b/crates/util/src/archive.rs deleted file mode 100644 index 5a5dc77772..0000000000 --- a/crates/util/src/archive.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::path::Path; - -use anyhow::{Context as _, Result}; -use async_zip::base::read; -#[cfg(not(windows))] -use futures::AsyncSeek; -use futures::{AsyncRead, io::BufReader}; - -#[cfg(windows)] -pub async fn extract_zip(destination: &Path, reader: R) -> Result<()> { - let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader)); - - let destination = &destination - .canonicalize() - .unwrap_or_else(|_| destination.to_path_buf()); - - while let Some(mut item) = reader.next_with_entry().await? { - let entry_reader = item.reader_mut(); - let entry = entry_reader.entry(); - let path = destination.join( - entry - .filename() - .as_str() - .context("reading zip entry file name")?, - ); - - if entry - .dir() - .with_context(|| format!("reading zip entry metadata for path {path:?}"))? - { - std::fs::create_dir_all(&path) - .with_context(|| format!("creating directory {path:?}"))?; - } else { - let parent_dir = path - .parent() - .with_context(|| format!("no parent directory for {path:?}"))?; - std::fs::create_dir_all(parent_dir) - .with_context(|| format!("creating parent directory {parent_dir:?}"))?; - let mut file = smol::fs::File::create(&path) - .await - .with_context(|| format!("creating file {path:?}"))?; - futures::io::copy(entry_reader, &mut file) - .await - .with_context(|| format!("extracting into file {path:?}"))?; - } - - reader = item.skip().await.context("reading next zip entry")?; - } - - Ok(()) -} - -#[cfg(not(windows))] -pub async fn extract_zip(destination: &Path, reader: R) -> Result<()> { - // Unix needs file permissions copied when extracting. - // This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used. - // `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`. - // - // A typical `reader` comes from a streaming network response, so cannot be sought right away, - // and reading the entire archive into the memory seems wasteful. - // - // So, save the stream into a temporary file first and then get it read with a seeking reader. - let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?); - futures::io::copy(&mut BufReader::new(reader), &mut file) - .await - .context("saving archive contents into the temporary file")?; - extract_seekable_zip(destination, file).await -} - -#[cfg(not(windows))] -pub async fn extract_seekable_zip( - destination: &Path, - reader: R, -) -> Result<()> { - let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader)) - .await - .context("reading the zip archive")?; - let destination = &destination - .canonicalize() - .unwrap_or_else(|_| destination.to_path_buf()); - for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() { - let path = destination.join( - entry - .filename() - .as_str() - .context("reading zip entry file name")?, - ); - - if entry - .dir() - .with_context(|| format!("reading zip entry metadata for path {path:?}"))? - { - std::fs::create_dir_all(&path) - .with_context(|| format!("creating directory {path:?}"))?; - } else { - let parent_dir = path - .parent() - .with_context(|| format!("no parent directory for {path:?}"))?; - std::fs::create_dir_all(parent_dir) - .with_context(|| format!("creating parent directory {parent_dir:?}"))?; - let mut file = smol::fs::File::create(&path) - .await - .with_context(|| format!("creating file {path:?}"))?; - let mut entry_reader = reader - .reader_with_entry(i) - .await - .with_context(|| format!("reading entry for path {path:?}"))?; - futures::io::copy(&mut entry_reader, &mut file) - .await - .with_context(|| format!("extracting into file {path:?}"))?; - - if let Some(perms) = entry.unix_permissions() { - use std::os::unix::fs::PermissionsExt; - let permissions = std::fs::Permissions::from_mode(u32::from(perms)); - file.set_permissions(permissions) - .await - .with_context(|| format!("setting permissions for file {path:?}"))?; - } - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use async_zip::ZipEntryBuilder; - use async_zip::base::write::ZipFileWriter; - use futures::{AsyncSeek, AsyncWriteExt}; - use smol::io::Cursor; - use tempfile::TempDir; - - use super::*; - - async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> { - let mut out = smol::fs::File::create(dst).await?; - let mut writer = ZipFileWriter::new(&mut out); - - for entry in walkdir::WalkDir::new(src_dir) { - let entry = entry?; - let path = entry.path(); - - if path.is_dir() { - continue; - } - - let relative_path = path.strip_prefix(src_dir)?; - let data = smol::fs::read(&path).await?; - - let filename = relative_path.display().to_string(); - - #[cfg(unix)] - { - let mut builder = - ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); - use std::os::unix::fs::PermissionsExt; - let metadata = std::fs::metadata(path)?; - let perms = metadata.permissions().mode() as u16; - builder = builder.unix_permissions(perms); - writer.write_entry_whole(builder, &data).await?; - } - #[cfg(not(unix))] - { - let builder = - ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate); - writer.write_entry_whole(builder, &data).await?; - } - } - - writer.close().await?; - out.flush().await?; - out.sync_all().await?; - - Ok(()) - } - - #[track_caller] - fn assert_file_content(path: &Path, content: &str) { - assert!(path.exists(), "file not found: {:?}", path); - let actual = std::fs::read_to_string(path).unwrap(); - assert_eq!(actual, content); - } - - #[track_caller] - fn make_test_data() -> TempDir { - let dir = tempfile::tempdir().unwrap(); - let dst = dir.path(); - - std::fs::write(dst.join("test"), "Hello world.").unwrap(); - std::fs::create_dir_all(dst.join("foo/bar")).unwrap(); - std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap(); - std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap(); - std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap(); - - dir - } - - async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin { - let data = smol::fs::read(&path).await.unwrap(); - Cursor::new(data) - } - - #[test] - fn test_extract_zip() { - let test_dir = make_test_data(); - let zip_file = test_dir.path().join("test.zip"); - - smol::block_on(async { - compress_zip(test_dir.path(), &zip_file).await.unwrap(); - let reader = read_archive(&zip_file).await; - - let dir = tempfile::tempdir().unwrap(); - let dst = dir.path(); - extract_zip(dst, reader).await.unwrap(); - - assert_file_content(&dst.join("test"), "Hello world."); - assert_file_content(&dst.join("foo/bar.txt"), "Foo bar."); - assert_file_content(&dst.join("foo/dar.md"), "Bar dar."); - assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界"); - }); - } - - #[cfg(unix)] - #[test] - fn test_extract_zip_preserves_executable_permissions() { - use std::os::unix::fs::PermissionsExt; - - smol::block_on(async { - let test_dir = tempfile::tempdir().unwrap(); - let executable_path = test_dir.path().join("my_script"); - - // Create an executable file - std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap(); - let mut perms = std::fs::metadata(&executable_path).unwrap().permissions(); - perms.set_mode(0o755); // rwxr-xr-x - std::fs::set_permissions(&executable_path, perms).unwrap(); - - // Create zip - let zip_file = test_dir.path().join("test.zip"); - compress_zip(test_dir.path(), &zip_file).await.unwrap(); - - // Extract to new location - let extract_dir = tempfile::tempdir().unwrap(); - let reader = read_archive(&zip_file).await; - extract_zip(extract_dir.path(), reader).await.unwrap(); - - // Check permissions are preserved - let extracted_path = extract_dir.path().join("my_script"); - assert!(extracted_path.exists()); - let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions(); - assert_eq!(extracted_perms.mode() & 0o777, 0o755); - }); - } -} diff --git a/crates/util/src/command.rs b/crates/util/src/command.rs deleted file mode 100644 index 40f1ec323f..0000000000 --- a/crates/util/src/command.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::ffi::OsStr; - -#[cfg(target_os = "windows")] -const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32; - -#[cfg(target_os = "windows")] -pub fn new_std_command(program: impl AsRef) -> std::process::Command { - use std::os::windows::process::CommandExt; - - let mut command = std::process::Command::new(program); - command.creation_flags(CREATE_NO_WINDOW); - command -} - -#[cfg(not(target_os = "windows"))] -pub fn new_std_command(program: impl AsRef) -> std::process::Command { - std::process::Command::new(program) -} - -#[cfg(target_os = "windows")] -pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { - use smol::process::windows::CommandExt; - - let mut command = smol::process::Command::new(program); - command.creation_flags(CREATE_NO_WINDOW); - command -} - -#[cfg(target_os = "macos")] -pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { - use std::os::unix::process::CommandExt; - - // Create a std::process::Command first so we can use pre_exec - let mut std_cmd = std::process::Command::new(program); - - // WORKAROUND: Reset exception ports before exec to prevent inheritance of - // crash handler exception ports. Due to a timing issue, child processes can - // inherit the parent's exception ports before they're fully stabilized, - // which can block child process spawning. - // See: https://github.com/zed-industries/zed/issues/36754 - unsafe { - std_cmd.pre_exec(|| { - // Reset all exception ports to system defaults for this task. - // This prevents the child from inheriting the parent's crash handler - // exception ports. - reset_exception_ports(); - Ok(()) - }); - } - - // Convert to async_process::Command via From trait - smol::process::Command::from(std_cmd) -} - -#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))] -pub fn new_smol_command(program: impl AsRef) -> smol::process::Command { - smol::process::Command::new(program) -} - -#[cfg(target_os = "macos")] -pub fn reset_exception_ports() { - use mach2::exception_types::{ - EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t, - }; - use mach2::kern_return::{KERN_SUCCESS, kern_return_t}; - use mach2::mach_types::task_t; - use mach2::port::{MACH_PORT_NULL, mach_port_t}; - use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t}; - use mach2::traps::mach_task_self; - - // FFI binding for task_set_exception_ports (not exposed by mach2 crate) - unsafe extern "C" { - fn task_set_exception_ports( - task: task_t, - exception_mask: exception_mask_t, - new_port: mach_port_t, - behavior: exception_behavior_t, - new_flavor: thread_state_flavor_t, - ) -> kern_return_t; - } - - unsafe { - let task = mach_task_self(); - // Reset all exception ports to MACH_PORT_NULL (system default) - // This prevents the child process from inheriting the parent's crash handler - let kr = task_set_exception_ports( - task, - EXC_MASK_ALL, - MACH_PORT_NULL, - EXCEPTION_DEFAULT as exception_behavior_t, - THREAD_STATE_NONE, - ); - - if kr != KERN_SUCCESS { - // Log but don't fail - the process can still work without this workaround - eprintln!( - "Warning: failed to reset exception ports in child process (kern_return: {})", - kr - ); - } - } -} diff --git a/crates/util/src/fs.rs b/crates/util/src/fs.rs deleted file mode 100644 index 60aab4a2e7..0000000000 --- a/crates/util/src/fs.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::ResultExt; -use anyhow::{Result, bail}; -use async_fs as fs; -use futures_lite::StreamExt; -use std::path::{Path, PathBuf}; - -/// Removes all files and directories matching the given predicate -pub async fn remove_matching(dir: &Path, predicate: F) -where - F: Fn(&Path) -> bool, -{ - if let Some(mut entries) = fs::read_dir(dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if predicate(entry_path.as_path()) - && let Ok(metadata) = fs::metadata(&entry_path).await - { - if metadata.is_file() { - fs::remove_file(&entry_path).await.log_err(); - } else { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } -} - -pub async fn collect_matching(dir: &Path, predicate: F) -> Vec -where - F: Fn(&Path) -> bool, -{ - let mut matching = vec![]; - - if let Some(mut entries) = fs::read_dir(dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() - && predicate(entry.path().as_path()) - { - matching.push(entry.path()); - } - } - } - - matching -} - -pub async fn find_file_name_in_dir(dir: &Path, predicate: F) -> Option -where - F: Fn(&str) -> bool, -{ - if let Some(mut entries) = fs::read_dir(dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - - if let Some(file_name) = entry_path - .file_name() - .map(|file_name| file_name.to_string_lossy()) - && predicate(&file_name) - { - return Some(entry_path); - } - } - } - } - - None -} - -pub async fn move_folder_files_to_folder>( - source_path: P, - target_path: P, -) -> Result<()> { - if !target_path.as_ref().is_dir() { - bail!("Folder not found or is not a directory"); - } - - let mut entries = fs::read_dir(source_path.as_ref()).await?; - while let Some(entry) = entries.next().await { - let entry = entry?; - let old_path = entry.path(); - let new_path = target_path.as_ref().join(entry.file_name()); - - fs::rename(&old_path, &new_path).await?; - } - - fs::remove_dir(source_path).await?; - - Ok(()) -} - -#[cfg(unix)] -/// Set the permissions for the given path so that the file becomes executable. -/// This is a noop for non-unix platforms. -pub async fn make_file_executable(path: &Path) -> std::io::Result<()> { - fs::set_permissions( - path, - ::from_mode(0o755), - ) - .await -} - -#[cfg(not(unix))] -#[allow(clippy::unused_async)] -/// Set the permissions for the given path so that the file becomes executable. -/// This is a noop for non-unix platforms. -pub async fn make_file_executable(_path: &Path) -> std::io::Result<()> { - Ok(()) -} diff --git a/crates/util/src/markdown.rs b/crates/util/src/markdown.rs deleted file mode 100644 index 303dbe0cf5..0000000000 --- a/crates/util/src/markdown.rs +++ /dev/null @@ -1,268 +0,0 @@ -use std::fmt::{Display, Formatter}; - -/// Indicates that the wrapped `String` is markdown text. -#[derive(Debug, Clone)] -pub struct MarkdownString(pub String); - -impl Display for MarkdownString { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -/// Escapes markdown special characters in markdown text blocks. Markdown code blocks follow -/// different rules and `MarkdownInlineCode` or `MarkdownCodeBlock` should be used in that case. -/// -/// Also escapes the following markdown extensions: -/// -/// * `^` for superscripts -/// * `$` for inline math -/// * `~` for strikethrough -/// -/// Escape of some characters is unnecessary, because while they are involved in markdown syntax, -/// the other characters involved are escaped: -/// -/// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as -/// plaintext. -/// -/// * `;` is used in HTML entity syntax, but `&` is escaped, so they are parsed as plaintext. -/// -/// TODO: There is one escape this doesn't do currently. Period after numbers at the start of the -/// line (`[0-9]*\.`) should also be escaped to avoid it being interpreted as a list item. -pub struct MarkdownEscaped<'a>(pub &'a str); - -/// Implements `Display` to format markdown inline code (wrapped in backticks), handling code that -/// contains backticks and spaces. All whitespace is treated as a single space character. For text -/// that does not contain whitespace other than ' ', this escaping roundtrips through -/// pulldown-cmark. -/// -/// When used in tables, `|` should be escaped like `\|` in the text provided to this function. -pub struct MarkdownInlineCode<'a>(pub &'a str); - -/// Implements `Display` to format markdown code blocks, wrapped in 3 or more backticks as needed. -pub struct MarkdownCodeBlock<'a> { - pub tag: &'a str, - pub text: &'a str, -} - -impl Display for MarkdownEscaped<'_> { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - let mut start_of_unescaped = None; - for (ix, c) in self.0.char_indices() { - match c { - // Always escaped. - '\\' | '`' | '*' | '_' | '[' | '^' | '$' | '~' | '&' | - // TODO: these only need to be escaped when they are the first non-whitespace - // character of the line of a block. There should probably be both an `escape_block` - // which does this and an `escape_inline` method which does not escape these. - '#' | '+' | '=' | '-' => { - match start_of_unescaped { - None => {} - Some(start_of_unescaped) => { - write!(formatter, "{}", &self.0[start_of_unescaped..ix])?; - } - } - write!(formatter, "\\")?; - // Can include this char in the "unescaped" text since a - // backslash was just emitted. - start_of_unescaped = Some(ix); - } - // Escaped since `<` is used in opening HTML tags. `<` is used since Markdown - // supports HTML entities, and this allows the text to be used directly in HTML. - '<' => { - match start_of_unescaped { - None => {} - Some(start_of_unescaped) => { - write!(formatter, "{}", &self.0[start_of_unescaped..ix])?; - } - } - write!(formatter, "<")?; - start_of_unescaped = None; - } - // Escaped since `>` is used for blockquotes. `>` is used since Markdown supports - // HTML entities, and this allows the text to be used directly in HTML. - '>' => { - match start_of_unescaped { - None => {} - Some(start_of_unescaped) => { - write!(formatter, "{}", &self.0[start_of_unescaped..ix])?; - } - } - write!(formatter, ">")?; - start_of_unescaped = None; - } - _ => { - if start_of_unescaped.is_none() { - start_of_unescaped = Some(ix); - } - } - } - } - if let Some(start_of_unescaped) = start_of_unescaped { - write!(formatter, "{}", &self.0[start_of_unescaped..])?; - } - Ok(()) - } -} - -impl Display for MarkdownInlineCode<'_> { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - // Apache License 2.0, same as this crate. - // - // Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications: - // - // * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating - // `Code` events parsed by pulldown-cmark. - // - // https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290 - - let mut all_whitespace = true; - let text = self - .0 - .chars() - .map(|c| { - if c.is_whitespace() { - ' ' - } else { - all_whitespace = false; - c - } - }) - .collect::(); - - // When inline code has leading and trailing ' ' characters, additional space is needed - // to escape it, unless all characters are space. - if all_whitespace { - write!(formatter, "`{text}`") - } else { - // More backticks are needed to delimit the inline code than the maximum number of - // backticks in a consecutive run. - let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1); - let space = match text.as_bytes() { - &[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick. - &[b' ', .., b' '] => " ", // Space needed to escape inner space. - _ => "", // No space needed. - }; - write!(formatter, "{backticks}{space}{text}{space}{backticks}") - } - } -} - -impl Display for MarkdownCodeBlock<'_> { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - let tag = self.tag; - let text = self.text; - let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1)); - write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n") - } -} - -// Copied from `pulldown-cmark-to-cmark-20.0.0` with changed names. -// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L1063 -// Apache License 2.0, same as this code. -fn count_max_consecutive_chars(text: &str, search: char) -> usize { - let mut in_search_chars = false; - let mut max_count = 0; - let mut cur_count = 0; - - for ch in text.chars() { - if ch == search { - cur_count += 1; - in_search_chars = true; - } else if in_search_chars { - max_count = max_count.max(cur_count); - cur_count = 0; - in_search_chars = false; - } - } - max_count.max(cur_count) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_markdown_escaped() { - let input = r#" - # Heading - - Another heading - === - - Another heading variant - --- - - Paragraph with [link](https://example.com) and `code`, *emphasis*, and ~strikethrough~. - - ``` - code block - ``` - - List with varying leaders: - - Item 1 - * Item 2 - + Item 3 - - Some math: $`\sqrt{3x-1}+(1+x)^2`$ - - HTML entity:   - "#; - - let expected = r#" - \# Heading - - Another heading - \=\=\= - - Another heading variant - \-\-\- - - Paragraph with \[link](https://example.com) and \`code\`, \*emphasis\*, and \~strikethrough\~. - - \`\`\` - code block - \`\`\` - - List with varying leaders: - \- Item 1 - \* Item 2 - \+ Item 3 - - Some math: \$\`\\sqrt{3x\-1}\+(1\+x)\^2\`\$ - - HTML entity: \  - "#; - - assert_eq!(MarkdownEscaped(input).to_string(), expected); - } - - #[test] - fn test_markdown_inline_code() { - assert_eq!(MarkdownInlineCode(" ").to_string(), "` `"); - assert_eq!(MarkdownInlineCode("text").to_string(), "`text`"); - assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `"); - assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `"); - assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``"); - assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```"); - assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``"); - assert_eq!( - MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(), - "``some `text` no leading or trailing backticks``" - ); - } - - #[test] - fn test_count_max_consecutive_chars() { - assert_eq!( - count_max_consecutive_chars("``a```b``", '`'), - 3, - "the highest seen consecutive segment of backticks counts" - ); - assert_eq!( - count_max_consecutive_chars("```a``b`", '`'), - 3, - "it can't be downgraded later" - ); - } -} diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs deleted file mode 100644 index a54f91c7a0..0000000000 --- a/crates/util/src/paths.rs +++ /dev/null @@ -1,2670 +0,0 @@ -use anyhow::Context; -use globset::{GlobBuilder, GlobSet, GlobSetBuilder}; -use itertools::Itertools; -use regex::Regex; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::cmp::Ordering; -use std::error::Error; -use std::fmt::{Display, Formatter}; -use std::mem; -use std::path::StripPrefixError; -use std::sync::{Arc, OnceLock}; -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, - sync::LazyLock, -}; - -use crate::rel_path::RelPathBuf; -use crate::{rel_path::RelPath, shell::ShellKind}; - -static HOME_DIR: OnceLock = OnceLock::new(); - -/// Returns the path to the user's home directory. -pub fn home_dir() -> &'static PathBuf { - HOME_DIR.get_or_init(|| { - if cfg!(any(test, feature = "test-support")) { - if cfg!(target_os = "macos") { - PathBuf::from("/Users/zed") - } else if cfg!(target_os = "windows") { - PathBuf::from("C:\\Users\\zed") - } else { - PathBuf::from("/home/zed") - } - } else { - dirs::home_dir().expect("failed to determine home directory") - } - }) -} - -pub trait PathExt { - /// Compacts a given file path by replacing the user's home directory - /// prefix with a tilde (`~`). - /// - /// # Returns - /// - /// * A `PathBuf` containing the compacted file path. If the input path - /// does not have the user's home directory prefix, or if we are not on - /// Linux or macOS, the original path is returned unchanged. - fn compact(&self) -> PathBuf; - - /// Returns a file's extension or, if the file is hidden, its name without the leading dot - fn extension_or_hidden_file_name(&self) -> Option<&str>; - - fn try_from_bytes<'a>(bytes: &'a [u8]) -> anyhow::Result - where - Self: From<&'a Path>, - { - #[cfg(unix)] - { - use std::os::unix::prelude::OsStrExt; - Ok(Self::from(Path::new(OsStr::from_bytes(bytes)))) - } - #[cfg(windows)] - { - use tendril::fmt::{Format, WTF8}; - WTF8::validate(bytes) - .then(|| { - // Safety: bytes are valid WTF-8 sequence. - Self::from(Path::new(unsafe { - OsStr::from_encoded_bytes_unchecked(bytes) - })) - }) - .with_context(|| format!("Invalid WTF-8 sequence: {bytes:?}")) - } - } - - /// Converts a local path to one that can be used inside of WSL. - /// Returns `None` if the path cannot be converted into a WSL one (network share). - fn local_to_wsl(&self) -> Option; - - /// Returns a file's "full" joined collection of extensions, in the case where a file does not - /// just have a singular extension but instead has multiple (e.g File.tar.gz, Component.stories.tsx) - /// - /// Will provide back the extensions joined together such as tar.gz or stories.tsx - fn multiple_extensions(&self) -> Option; - - /// Try to make a shell-safe representation of the path. - fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result; -} - -impl> PathExt for T { - fn compact(&self) -> PathBuf { - if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") { - match self.as_ref().strip_prefix(home_dir().as_path()) { - Ok(relative_path) => { - let mut shortened_path = PathBuf::new(); - shortened_path.push("~"); - shortened_path.push(relative_path); - shortened_path - } - Err(_) => self.as_ref().to_path_buf(), - } - } else { - self.as_ref().to_path_buf() - } - } - - fn extension_or_hidden_file_name(&self) -> Option<&str> { - let path = self.as_ref(); - let file_name = path.file_name()?.to_str()?; - if file_name.starts_with('.') { - return file_name.strip_prefix('.'); - } - - path.extension() - .and_then(|e| e.to_str()) - .or_else(|| path.file_stem()?.to_str()) - } - - fn local_to_wsl(&self) -> Option { - // quite sketchy to convert this back to path at the end, but a lot of functions only accept paths - // todo: ideally rework them..? - let mut new_path = std::ffi::OsString::new(); - for component in self.as_ref().components() { - match component { - std::path::Component::Prefix(prefix) => { - let drive_letter = prefix.as_os_str().to_string_lossy().to_lowercase(); - let drive_letter = drive_letter.strip_suffix(':')?; - - new_path.push(format!("/mnt/{}", drive_letter)); - } - std::path::Component::RootDir => {} - std::path::Component::CurDir => { - new_path.push("/."); - } - std::path::Component::ParentDir => { - new_path.push("/.."); - } - std::path::Component::Normal(os_str) => { - new_path.push("/"); - new_path.push(os_str); - } - } - } - - Some(new_path.into()) - } - - fn multiple_extensions(&self) -> Option { - let path = self.as_ref(); - let file_name = path.file_name()?.to_str()?; - - let parts: Vec<&str> = file_name - .split('.') - // Skip the part with the file name extension - .skip(1) - .collect(); - - if parts.len() < 2 { - return None; - } - - Some(parts.into_iter().join(".")) - } - - fn try_shell_safe(&self, shell_kind: ShellKind) -> anyhow::Result { - let path_str = self - .as_ref() - .to_str() - .with_context(|| "Path contains invalid UTF-8")?; - shell_kind - .try_quote(path_str) - .as_deref() - .map(ToOwned::to_owned) - .context("Failed to quote path") - } -} - -pub fn path_ends_with(base: &Path, suffix: &Path) -> bool { - strip_path_suffix(base, suffix).is_some() -} - -pub fn strip_path_suffix<'a>(base: &'a Path, suffix: &Path) -> Option<&'a Path> { - if let Some(remainder) = base - .as_os_str() - .as_encoded_bytes() - .strip_suffix(suffix.as_os_str().as_encoded_bytes()) - { - if remainder - .last() - .is_none_or(|last_byte| std::path::is_separator(*last_byte as char)) - { - let os_str = unsafe { - OsStr::from_encoded_bytes_unchecked( - &remainder[0..remainder.len().saturating_sub(1)], - ) - }; - return Some(Path::new(os_str)); - } - } - None -} - -/// In memory, this is identical to `Path`. On non-Windows conversions to this type are no-ops. On -/// windows, these conversions sanitize UNC paths by removing the `\\\\?\\` prefix. -#[derive(Eq, PartialEq, Hash, Ord, PartialOrd)] -#[repr(transparent)] -pub struct SanitizedPath(Path); - -impl SanitizedPath { - pub fn new + ?Sized>(path: &T) -> &Self { - #[cfg(not(target_os = "windows"))] - return Self::unchecked_new(path.as_ref()); - - #[cfg(target_os = "windows")] - return Self::unchecked_new(dunce::simplified(path.as_ref())); - } - - pub fn unchecked_new + ?Sized>(path: &T) -> &Self { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::<&Path, &Self>(path.as_ref()) } - } - - pub fn from_arc(path: Arc) -> Arc { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - #[cfg(not(target_os = "windows"))] - return unsafe { mem::transmute::, Arc>(path) }; - - #[cfg(target_os = "windows")] - { - let simplified = dunce::simplified(path.as_ref()); - if simplified == path.as_ref() { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::, Arc>(path) } - } else { - Self::unchecked_new(simplified).into() - } - } - } - - pub fn new_arc + ?Sized>(path: &T) -> Arc { - Self::new(path).into() - } - - pub fn cast_arc(path: Arc) -> Arc { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::, Arc>(path) } - } - - pub fn cast_arc_ref(path: &Arc) -> &Arc { - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute::<&Arc, &Arc>(path) } - } - - pub fn starts_with(&self, prefix: &Self) -> bool { - self.0.starts_with(&prefix.0) - } - - pub fn as_path(&self) -> &Path { - &self.0 - } - - pub fn file_name(&self) -> Option<&std::ffi::OsStr> { - self.0.file_name() - } - - pub fn extension(&self) -> Option<&std::ffi::OsStr> { - self.0.extension() - } - - pub fn join>(&self, path: P) -> PathBuf { - self.0.join(path) - } - - pub fn parent(&self) -> Option<&Self> { - self.0.parent().map(Self::unchecked_new) - } - - pub fn strip_prefix(&self, base: &Self) -> Result<&Path, StripPrefixError> { - self.0.strip_prefix(base.as_path()) - } - - pub fn to_str(&self) -> Option<&str> { - self.0.to_str() - } - - pub fn to_path_buf(&self) -> PathBuf { - self.0.to_path_buf() - } -} - -impl std::fmt::Debug for SanitizedPath { - fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(&self.0, formatter) - } -} - -impl Display for SanitizedPath { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0.display()) - } -} - -impl From<&SanitizedPath> for Arc { - fn from(sanitized_path: &SanitizedPath) -> Self { - let path: Arc = sanitized_path.0.into(); - // safe because `Path` and `SanitizedPath` have the same repr and Drop impl - unsafe { mem::transmute(path) } - } -} - -impl From<&SanitizedPath> for PathBuf { - fn from(sanitized_path: &SanitizedPath) -> Self { - sanitized_path.as_path().into() - } -} - -impl AsRef for SanitizedPath { - fn as_ref(&self) -> &Path { - &self.0 - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum PathStyle { - Posix, - Windows, -} - -impl PathStyle { - #[cfg(target_os = "windows")] - pub const fn local() -> Self { - PathStyle::Windows - } - - #[cfg(not(target_os = "windows"))] - pub const fn local() -> Self { - PathStyle::Posix - } - - #[inline] - pub fn primary_separator(&self) -> &'static str { - match self { - PathStyle::Posix => "/", - PathStyle::Windows => "\\", - } - } - - pub fn separators(&self) -> &'static [&'static str] { - match self { - PathStyle::Posix => &["/"], - PathStyle::Windows => &["\\", "/"], - } - } - - pub fn separators_ch(&self) -> &'static [char] { - match self { - PathStyle::Posix => &['/'], - PathStyle::Windows => &['\\', '/'], - } - } - - pub fn is_windows(&self) -> bool { - *self == PathStyle::Windows - } - - pub fn is_posix(&self) -> bool { - *self == PathStyle::Posix - } - - pub fn join(self, left: impl AsRef, right: impl AsRef) -> Option { - let right = right.as_ref().to_str()?; - if is_absolute(right, self) { - return None; - } - let left = left.as_ref().to_str()?; - if left.is_empty() { - Some(right.into()) - } else { - Some(format!( - "{left}{}{right}", - if left.ends_with(self.primary_separator()) { - "" - } else { - self.primary_separator() - } - )) - } - } - - pub fn split(self, path_like: &str) -> (Option<&str>, &str) { - let Some(pos) = path_like.rfind(self.primary_separator()) else { - return (None, path_like); - }; - let filename_start = pos + self.primary_separator().len(); - ( - Some(&path_like[..filename_start]), - &path_like[filename_start..], - ) - } - - pub fn strip_prefix<'a>( - &self, - child: &'a Path, - parent: &'a Path, - ) -> Option> { - let parent = parent.to_str()?; - if parent.is_empty() { - return RelPath::new(child, *self).ok(); - } - let parent = self - .separators() - .iter() - .find_map(|sep| parent.strip_suffix(sep)) - .unwrap_or(parent); - let child = child.to_str()?; - let stripped = child.strip_prefix(parent)?; - if let Some(relative) = self - .separators() - .iter() - .find_map(|sep| stripped.strip_prefix(sep)) - { - RelPath::new(relative.as_ref(), *self).ok() - } else if stripped.is_empty() { - Some(Cow::Borrowed(RelPath::empty())) - } else { - None - } - } -} - -#[derive(Debug, Clone)] -pub struct RemotePathBuf { - style: PathStyle, - string: String, -} - -impl RemotePathBuf { - pub fn new(string: String, style: PathStyle) -> Self { - Self { style, string } - } - - pub fn from_str(path: &str, style: PathStyle) -> Self { - Self::new(path.to_string(), style) - } - - pub fn path_style(&self) -> PathStyle { - self.style - } - - pub fn to_proto(self) -> String { - self.string - } -} - -impl Display for RemotePathBuf { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.string) - } -} - -pub fn is_absolute(path_like: &str, path_style: PathStyle) -> bool { - path_like.starts_with('/') - || path_style == PathStyle::Windows - && (path_like.starts_with('\\') - || path_like - .chars() - .next() - .is_some_and(|c| c.is_ascii_alphabetic()) - && path_like[1..] - .strip_prefix(':') - .is_some_and(|path| path.starts_with('/') || path.starts_with('\\'))) -} - -#[derive(Debug, PartialEq)] -#[non_exhaustive] -pub struct NormalizeError; - -impl Error for NormalizeError {} - -impl std::fmt::Display for NormalizeError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("parent reference `..` points outside of base directory") - } -} - -/// Copied from stdlib where it's unstable. -/// -/// Normalize a path, including `..` without traversing the filesystem. -/// -/// Returns an error if normalization would leave leading `..` components. -/// -///
-/// -/// This function always resolves `..` to the "lexical" parent. -/// That is "a/b/../c" will always resolve to `a/c` which can change the meaning of the path. -/// In particular, `a/c` and `a/b/../c` are distinct on many systems because `b` may be a symbolic link, so its parent isn't `a`. -/// -///
-/// -/// [`path::absolute`](absolute) is an alternative that preserves `..`. -/// Or [`Path::canonicalize`] can be used to resolve any `..` by querying the filesystem. -pub fn normalize_lexically(path: &Path) -> Result { - use std::path::Component; - - let mut lexical = PathBuf::new(); - let mut iter = path.components().peekable(); - - // Find the root, if any, and add it to the lexical path. - // Here we treat the Windows path "C:\" as a single "root" even though - // `components` splits it into two: (Prefix, RootDir). - let root = match iter.peek() { - Some(Component::ParentDir) => return Err(NormalizeError), - Some(p @ Component::RootDir) | Some(p @ Component::CurDir) => { - lexical.push(p); - iter.next(); - lexical.as_os_str().len() - } - Some(Component::Prefix(prefix)) => { - lexical.push(prefix.as_os_str()); - iter.next(); - if let Some(p @ Component::RootDir) = iter.peek() { - lexical.push(p); - iter.next(); - } - lexical.as_os_str().len() - } - None => return Ok(PathBuf::new()), - Some(Component::Normal(_)) => 0, - }; - - for component in iter { - match component { - Component::RootDir => unreachable!(), - Component::Prefix(_) => return Err(NormalizeError), - Component::CurDir => continue, - Component::ParentDir => { - // It's an error if ParentDir causes us to go above the "root". - if lexical.as_os_str().len() == root { - return Err(NormalizeError); - } else { - lexical.pop(); - } - } - Component::Normal(path) => lexical.push(path), - } - } - Ok(lexical) -} - -/// A delimiter to use in `path_query:row_number:column_number` strings parsing. -pub const FILE_ROW_COLUMN_DELIMITER: char = ':'; - -const ROW_COL_CAPTURE_REGEX: &str = r"(?xs) - ([^\(]+)\:(?: - \((\d+)[,:](\d+)\) # filename:(row,column), filename:(row:column) - | - \((\d+)\)() # filename:(row) - ) - | - ([^\(]+)(?: - \((\d+)[,:](\d+)\) # filename(row,column), filename(row:column) - | - \((\d+)\)() # filename(row) - ) - | - (.+?)(?: - \:+(\d+)\:(\d+)\:*$ # filename:row:column - | - \:+(\d+)\:*()$ # filename:row - | - \:+()()$ - )"; - -/// A representation of a path-like string with optional row and column numbers. -/// Matching values example: `te`, `test.rs:22`, `te:22:5`, `test.c(22)`, `test.c(22,5)`etc. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub struct PathWithPosition { - pub path: PathBuf, - pub row: Option, - // Absent if row is absent. - pub column: Option, -} - -impl PathWithPosition { - /// Returns a PathWithPosition from a path. - pub fn from_path(path: PathBuf) -> Self { - Self { - path, - row: None, - column: None, - } - } - - /// Parses a string that possibly has `:row:column` or `(row, column)` suffix. - /// Parenthesis format is used by [MSBuild](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks) compatible tools - /// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`. - /// If the suffix parsing fails, the whole string is parsed as a path. - /// - /// Be mindful that `test_file:10:1:` is a valid posix filename. - /// `PathWithPosition` class assumes that the ending position-like suffix is **not** part of the filename. - /// - /// # Examples - /// - /// ``` - /// # use util::paths::PathWithPosition; - /// # use std::path::PathBuf; - /// assert_eq!(PathWithPosition::parse_str("test_file"), PathWithPosition { - /// path: PathBuf::from("test_file"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file:10"), PathWithPosition { - /// path: PathBuf::from("test_file"), - /// row: Some(10), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: Some(2), - /// }); - /// ``` - /// - /// # Expected parsing results when encounter ill-formatted inputs. - /// ``` - /// # use util::paths::PathWithPosition; - /// # use std::path::PathBuf; - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:a"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:a:b"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:a:b"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: None, - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs::1:2"), PathWithPosition { - /// path: PathBuf::from("test_file.rs"), - /// row: Some(1), - /// column: Some(2), - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1::2"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:1"), - /// row: Some(2), - /// column: None, - /// }); - /// assert_eq!(PathWithPosition::parse_str("test_file.rs:1:2:3"), PathWithPosition { - /// path: PathBuf::from("test_file.rs:1"), - /// row: Some(2), - /// column: Some(3), - /// }); - /// ``` - pub fn parse_str(s: &str) -> Self { - let trimmed = s.trim(); - let path = Path::new(trimmed); - let Some(maybe_file_name_with_row_col) = path.file_name().unwrap_or_default().to_str() - else { - return Self { - path: Path::new(s).to_path_buf(), - row: None, - column: None, - }; - }; - if maybe_file_name_with_row_col.is_empty() { - return Self { - path: Path::new(s).to_path_buf(), - row: None, - column: None, - }; - } - - // Let's avoid repeated init cost on this. It is subject to thread contention, but - // so far this code isn't called from multiple hot paths. Getting contention here - // in the future seems unlikely. - static SUFFIX_RE: LazyLock = - LazyLock::new(|| Regex::new(ROW_COL_CAPTURE_REGEX).unwrap()); - match SUFFIX_RE - .captures(maybe_file_name_with_row_col) - .map(|caps| caps.extract()) - { - Some((_, [file_name, maybe_row, maybe_column])) => { - let row = maybe_row.parse::().ok(); - let column = maybe_column.parse::().ok(); - - let (_, suffix) = trimmed.split_once(file_name).unwrap(); - let path_without_suffix = &trimmed[..trimmed.len() - suffix.len()]; - - Self { - path: Path::new(path_without_suffix).to_path_buf(), - row, - column, - } - } - None => { - // The `ROW_COL_CAPTURE_REGEX` deals with separated digits only, - // but in reality there could be `foo/bar.py:22:in` inputs which we want to match too. - // The regex mentioned is not very extendable with "digit or random string" checks, so do this here instead. - let delimiter = ':'; - let mut path_parts = s - .rsplitn(3, delimiter) - .collect::>() - .into_iter() - .rev() - .fuse(); - let mut path_string = path_parts.next().expect("rsplitn should have the rest of the string as its last parameter that we reversed").to_owned(); - let mut row = None; - let mut column = None; - if let Some(maybe_row) = path_parts.next() { - if let Ok(parsed_row) = maybe_row.parse::() { - row = Some(parsed_row); - if let Some(parsed_column) = path_parts - .next() - .and_then(|maybe_col| maybe_col.parse::().ok()) - { - column = Some(parsed_column); - } - } else { - path_string.push(delimiter); - path_string.push_str(maybe_row); - } - } - for split in path_parts { - path_string.push(delimiter); - path_string.push_str(split); - } - - Self { - path: PathBuf::from(path_string), - row, - column, - } - } - } - } - - pub fn map_path( - self, - mapping: impl FnOnce(PathBuf) -> Result, - ) -> Result { - Ok(PathWithPosition { - path: mapping(self.path)?, - row: self.row, - column: self.column, - }) - } - - pub fn to_string(&self, path_to_string: impl Fn(&PathBuf) -> String) -> String { - let path_string = path_to_string(&self.path); - if let Some(row) = self.row { - if let Some(column) = self.column { - format!("{path_string}:{row}:{column}") - } else { - format!("{path_string}:{row}") - } - } else { - path_string - } - } -} - -#[derive(Clone, Debug)] -pub struct PathMatcher { - sources: Vec<(String, RelPathBuf, /*trailing separator*/ bool)>, - glob: GlobSet, - path_style: PathStyle, -} - -impl PartialEq for PathMatcher { - fn eq(&self, other: &Self) -> bool { - self.sources.eq(&other.sources) - } -} - -impl Eq for PathMatcher {} - -impl PathMatcher { - pub fn new( - globs: impl IntoIterator>, - path_style: PathStyle, - ) -> Result { - let globs = globs - .into_iter() - .map(|as_str| { - GlobBuilder::new(as_str.as_ref()) - .backslash_escape(path_style.is_posix()) - .build() - }) - .collect::, _>>()?; - let sources = globs - .iter() - .filter_map(|glob| { - let glob = glob.glob(); - Some(( - glob.to_string(), - RelPath::new(&glob.as_ref(), path_style) - .ok() - .map(std::borrow::Cow::into_owned)?, - glob.ends_with(path_style.separators_ch()), - )) - }) - .collect(); - let mut glob_builder = GlobSetBuilder::new(); - for single_glob in globs { - glob_builder.add(single_glob); - } - let glob = glob_builder.build()?; - Ok(PathMatcher { - glob, - sources, - path_style, - }) - } - - pub fn sources(&self) -> impl Iterator + Clone { - self.sources.iter().map(|(source, ..)| source.as_str()) - } - - pub fn is_match>(&self, other: P) -> bool { - if self.sources.iter().any(|(_, source, _)| { - other.as_ref().starts_with(source) || other.as_ref().ends_with(source) - }) { - return true; - } - let other_path = other.as_ref().display(self.path_style); - - if self.glob.is_match(&*other_path) { - return true; - } - - self.glob - .is_match(other_path.into_owned() + self.path_style.primary_separator()) - } -} - -impl Default for PathMatcher { - fn default() -> Self { - Self { - path_style: PathStyle::local(), - glob: GlobSet::empty(), - sources: vec![], - } - } -} - -/// Compares two sequences of consecutive digits for natural sorting. -/// -/// This function is a core component of natural sorting that handles numeric comparison -/// in a way that feels natural to humans. It extracts and compares consecutive digit -/// sequences from two iterators, handling various cases like leading zeros and very large numbers. -/// -/// # Behavior -/// -/// The function implements the following comparison rules: -/// 1. Different numeric values: Compares by actual numeric value (e.g., "2" < "10") -/// 2. Leading zeros: When values are equal, longer sequence wins (e.g., "002" > "2") -/// 3. Large numbers: Falls back to string comparison for numbers that would overflow u128 -/// -/// # Examples -/// -/// ```text -/// "1" vs "2" -> Less (different values) -/// "2" vs "10" -> Less (numeric comparison) -/// "002" vs "2" -> Greater (leading zeros) -/// "10" vs "010" -> Less (leading zeros) -/// "999..." vs "1000..." -> Less (large number comparison) -/// ``` -/// -/// # Implementation Details -/// -/// 1. Extracts consecutive digits into strings -/// 2. Compares sequence lengths for leading zero handling -/// 3. For equal lengths, compares digit by digit -/// 4. For different lengths: -/// - Attempts numeric comparison first (for numbers up to 2^128 - 1) -/// - Falls back to string comparison if numbers would overflow -/// -/// The function advances both iterators past their respective numeric sequences, -/// regardless of the comparison result. -fn compare_numeric_segments( - a_iter: &mut std::iter::Peekable, - b_iter: &mut std::iter::Peekable, -) -> Ordering -where - I: Iterator, -{ - // Collect all consecutive digits into strings - let mut a_num_str = String::new(); - let mut b_num_str = String::new(); - - while let Some(&c) = a_iter.peek() { - if !c.is_ascii_digit() { - break; - } - - a_num_str.push(c); - a_iter.next(); - } - - while let Some(&c) = b_iter.peek() { - if !c.is_ascii_digit() { - break; - } - - b_num_str.push(c); - b_iter.next(); - } - - // First compare lengths (handle leading zeros) - match a_num_str.len().cmp(&b_num_str.len()) { - Ordering::Equal => { - // Same length, compare digit by digit - match a_num_str.cmp(&b_num_str) { - Ordering::Equal => Ordering::Equal, - ordering => ordering, - } - } - - // Different lengths but same value means leading zeros - ordering => { - // Try parsing as numbers first - if let (Ok(a_val), Ok(b_val)) = (a_num_str.parse::(), b_num_str.parse::()) { - match a_val.cmp(&b_val) { - Ordering::Equal => ordering, // Same value, longer one is greater (leading zeros) - ord => ord, - } - } else { - // If parsing fails (overflow), compare as strings - a_num_str.cmp(&b_num_str) - } - } - } -} - -/// Performs natural sorting comparison between two strings. -/// -/// Natural sorting is an ordering that handles numeric sequences in a way that matches human expectations. -/// For example, "file2" comes before "file10" (unlike standard lexicographic sorting). -/// -/// # Characteristics -/// -/// * Case-sensitive with lowercase priority: When comparing same letters, lowercase comes before uppercase -/// * Numbers are compared by numeric value, not character by character -/// * Leading zeros affect ordering when numeric values are equal -/// * Can handle numbers larger than u128::MAX (falls back to string comparison) -/// * When strings are equal case-insensitively, lowercase is prioritized (lowercase < uppercase) -/// -/// # Algorithm -/// -/// The function works by: -/// 1. Processing strings character by character in a case-insensitive manner -/// 2. When encountering digits, treating consecutive digits as a single number -/// 3. Comparing numbers by their numeric value rather than lexicographically -/// 4. For non-numeric characters, using case-insensitive comparison -/// 5. If everything is equal case-insensitively, using case-sensitive comparison as final tie-breaker -pub fn natural_sort(a: &str, b: &str) -> Ordering { - let mut a_iter = a.chars().peekable(); - let mut b_iter = b.chars().peekable(); - - loop { - match (a_iter.peek(), b_iter.peek()) { - (None, None) => { - return b.cmp(a); - } - (None, _) => return Ordering::Less, - (_, None) => return Ordering::Greater, - (Some(&a_char), Some(&b_char)) => { - if a_char.is_ascii_digit() && b_char.is_ascii_digit() { - match compare_numeric_segments(&mut a_iter, &mut b_iter) { - Ordering::Equal => continue, - ordering => return ordering, - } - } else { - match a_char - .to_ascii_lowercase() - .cmp(&b_char.to_ascii_lowercase()) - { - Ordering::Equal => { - a_iter.next(); - b_iter.next(); - } - ordering => return ordering, - } - } - } - } - } -} - -/// Case-insensitive natural sort without applying the final lowercase/uppercase tie-breaker. -/// This is useful when comparing individual path components where we want to keep walking -/// deeper components before deciding on casing. -fn natural_sort_no_tiebreak(a: &str, b: &str) -> Ordering { - if a.eq_ignore_ascii_case(b) { - Ordering::Equal - } else { - natural_sort(a, b) - } -} - -fn stem_and_extension(filename: &str) -> (Option<&str>, Option<&str>) { - if filename.is_empty() { - return (None, None); - } - - match filename.rsplit_once('.') { - // Case 1: No dot was found. The entire name is the stem. - None => (Some(filename), None), - - // Case 2: A dot was found. - Some((before, after)) => { - // This is the crucial check for dotfiles like ".bashrc". - // If `before` is empty, the dot was the first character. - // In that case, we revert to the "whole name is the stem" logic. - if before.is_empty() { - (Some(filename), None) - } else { - // Otherwise, we have a standard stem and extension. - (Some(before), Some(after)) - } - } - } -} - -pub fn compare_rel_paths( - (path_a, a_is_file): (&RelPath, bool), - (path_b, b_is_file): (&RelPath, bool), -) -> Ordering { - let mut components_a = path_a.components(); - let mut components_b = path_b.components(); - loop { - match (components_a.next(), components_b.next()) { - (Some(component_a), Some(component_b)) => { - let a_is_file = a_is_file && components_a.rest().is_empty(); - let b_is_file = b_is_file && components_b.rest().is_empty(); - - let ordering = a_is_file.cmp(&b_is_file).then_with(|| { - let (a_stem, a_extension) = a_is_file - .then(|| stem_and_extension(component_a)) - .unwrap_or_default(); - let path_string_a = if a_is_file { a_stem } else { Some(component_a) }; - - let (b_stem, b_extension) = b_is_file - .then(|| stem_and_extension(component_b)) - .unwrap_or_default(); - let path_string_b = if b_is_file { b_stem } else { Some(component_b) }; - - let compare_components = match (path_string_a, path_string_b) { - (Some(a), Some(b)) => natural_sort(&a, &b), - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => Ordering::Equal, - }; - - compare_components.then_with(|| { - if a_is_file && b_is_file { - let ext_a = a_extension.unwrap_or_default(); - let ext_b = b_extension.unwrap_or_default(); - ext_a.cmp(ext_b) - } else { - Ordering::Equal - } - }) - }); - - if !ordering.is_eq() { - return ordering; - } - } - (Some(_), None) => break Ordering::Greater, - (None, Some(_)) => break Ordering::Less, - (None, None) => break Ordering::Equal, - } - } -} - -/// Compare two relative paths with mixed files and directories using -/// case-insensitive natural sorting. For example, "Apple", "aardvark.txt", -/// and "Zebra" would be sorted as: aardvark.txt, Apple, Zebra -/// (case-insensitive alphabetical). -pub fn compare_rel_paths_mixed( - (path_a, a_is_file): (&RelPath, bool), - (path_b, b_is_file): (&RelPath, bool), -) -> Ordering { - let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b; - let mut components_a = path_a.components(); - let mut components_b = path_b.components(); - - loop { - match (components_a.next(), components_b.next()) { - (Some(component_a), Some(component_b)) => { - let a_leaf_file = a_is_file && components_a.rest().is_empty(); - let b_leaf_file = b_is_file && components_b.rest().is_empty(); - - let (a_stem, a_ext) = a_leaf_file - .then(|| stem_and_extension(component_a)) - .unwrap_or_default(); - let (b_stem, b_ext) = b_leaf_file - .then(|| stem_and_extension(component_b)) - .unwrap_or_default(); - let a_key = if a_leaf_file { - a_stem - } else { - Some(component_a) - }; - let b_key = if b_leaf_file { - b_stem - } else { - Some(component_b) - }; - - let ordering = match (a_key, b_key) { - (Some(a), Some(b)) => natural_sort_no_tiebreak(a, b) - .then_with(|| match (a_leaf_file, b_leaf_file) { - (true, false) if a == b => Ordering::Greater, - (false, true) if a == b => Ordering::Less, - _ => Ordering::Equal, - }) - .then_with(|| { - if a_leaf_file && b_leaf_file { - let a_ext_str = a_ext.unwrap_or_default().to_lowercase(); - let b_ext_str = b_ext.unwrap_or_default().to_lowercase(); - b_ext_str.cmp(&a_ext_str) - } else { - Ordering::Equal - } - }), - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => Ordering::Equal, - }; - - if !ordering.is_eq() { - return ordering; - } - } - (Some(_), None) => return Ordering::Greater, - (None, Some(_)) => return Ordering::Less, - (None, None) => { - // Deterministic tie-break: use natural sort to prefer lowercase when paths - // are otherwise equal but still differ in casing. - if !original_paths_equal { - return natural_sort(path_a.as_unix_str(), path_b.as_unix_str()); - } - return Ordering::Equal; - } - } - } -} - -/// Compare two relative paths with files before directories using -/// case-insensitive natural sorting. At each directory level, all files -/// are sorted before all directories, with case-insensitive alphabetical -/// ordering within each group. -pub fn compare_rel_paths_files_first( - (path_a, a_is_file): (&RelPath, bool), - (path_b, b_is_file): (&RelPath, bool), -) -> Ordering { - let original_paths_equal = std::ptr::eq(path_a, path_b) || path_a == path_b; - let mut components_a = path_a.components(); - let mut components_b = path_b.components(); - - loop { - match (components_a.next(), components_b.next()) { - (Some(component_a), Some(component_b)) => { - let a_leaf_file = a_is_file && components_a.rest().is_empty(); - let b_leaf_file = b_is_file && components_b.rest().is_empty(); - - let (a_stem, a_ext) = a_leaf_file - .then(|| stem_and_extension(component_a)) - .unwrap_or_default(); - let (b_stem, b_ext) = b_leaf_file - .then(|| stem_and_extension(component_b)) - .unwrap_or_default(); - let a_key = if a_leaf_file { - a_stem - } else { - Some(component_a) - }; - let b_key = if b_leaf_file { - b_stem - } else { - Some(component_b) - }; - - let ordering = match (a_key, b_key) { - (Some(a), Some(b)) => { - if a_leaf_file && !b_leaf_file { - Ordering::Less - } else if !a_leaf_file && b_leaf_file { - Ordering::Greater - } else { - natural_sort_no_tiebreak(a, b).then_with(|| { - if a_leaf_file && b_leaf_file { - let a_ext_str = a_ext.unwrap_or_default().to_lowercase(); - let b_ext_str = b_ext.unwrap_or_default().to_lowercase(); - a_ext_str.cmp(&b_ext_str) - } else { - Ordering::Equal - } - }) - } - } - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => Ordering::Equal, - }; - - if !ordering.is_eq() { - return ordering; - } - } - (Some(_), None) => return Ordering::Greater, - (None, Some(_)) => return Ordering::Less, - (None, None) => { - // Deterministic tie-break: use natural sort to prefer lowercase when paths - // are otherwise equal but still differ in casing. - if !original_paths_equal { - return natural_sort(path_a.as_unix_str(), path_b.as_unix_str()); - } - return Ordering::Equal; - } - } - } -} - -pub fn compare_paths( - (path_a, a_is_file): (&Path, bool), - (path_b, b_is_file): (&Path, bool), -) -> Ordering { - let mut components_a = path_a.components().peekable(); - let mut components_b = path_b.components().peekable(); - - loop { - match (components_a.next(), components_b.next()) { - (Some(component_a), Some(component_b)) => { - let a_is_file = components_a.peek().is_none() && a_is_file; - let b_is_file = components_b.peek().is_none() && b_is_file; - - let ordering = a_is_file.cmp(&b_is_file).then_with(|| { - let path_a = Path::new(component_a.as_os_str()); - let path_string_a = if a_is_file { - path_a.file_stem() - } else { - path_a.file_name() - } - .map(|s| s.to_string_lossy()); - - let path_b = Path::new(component_b.as_os_str()); - let path_string_b = if b_is_file { - path_b.file_stem() - } else { - path_b.file_name() - } - .map(|s| s.to_string_lossy()); - - let compare_components = match (path_string_a, path_string_b) { - (Some(a), Some(b)) => natural_sort(&a, &b), - (Some(_), None) => Ordering::Greater, - (None, Some(_)) => Ordering::Less, - (None, None) => Ordering::Equal, - }; - - compare_components.then_with(|| { - if a_is_file && b_is_file { - let ext_a = path_a.extension().unwrap_or_default(); - let ext_b = path_b.extension().unwrap_or_default(); - ext_a.cmp(ext_b) - } else { - Ordering::Equal - } - }) - }); - - if !ordering.is_eq() { - return ordering; - } - } - (Some(_), None) => break Ordering::Greater, - (None, Some(_)) => break Ordering::Less, - (None, None) => break Ordering::Equal, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct WslPath { - pub distro: String, - - // the reason this is an OsString and not any of the path types is that it needs to - // represent a unix path (with '/' separators) on windows. `from_path` does this by - // manually constructing it from the path components of a given windows path. - pub path: std::ffi::OsString, -} - -impl WslPath { - pub fn from_path>(path: P) -> Option { - if cfg!(not(target_os = "windows")) { - return None; - } - use std::{ - ffi::OsString, - path::{Component, Prefix}, - }; - - let mut components = path.as_ref().components(); - let Some(Component::Prefix(prefix)) = components.next() else { - return None; - }; - let (server, distro) = match prefix.kind() { - Prefix::UNC(server, distro) => (server, distro), - Prefix::VerbatimUNC(server, distro) => (server, distro), - _ => return None, - }; - let Some(Component::RootDir) = components.next() else { - return None; - }; - - let server_str = server.to_string_lossy(); - if server_str == "wsl.localhost" || server_str == "wsl$" { - let mut result = OsString::from(""); - for c in components { - use Component::*; - match c { - Prefix(p) => unreachable!("got {p:?}, but already stripped prefix"), - RootDir => unreachable!("got root dir, but already stripped root"), - CurDir => continue, - ParentDir => result.push("/.."), - Normal(s) => { - result.push("/"); - result.push(s); - } - } - } - if result.is_empty() { - result.push("/"); - } - Some(WslPath { - distro: distro.to_string_lossy().to_string(), - path: result, - }) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use crate::rel_path::rel_path; - - use super::*; - use util_macros::perf; - - #[perf] - fn compare_paths_with_dots() { - let mut paths = vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/1.46"), false), - (Path::new("test_dirs/1.46/bar_1"), true), - (Path::new("test_dirs/1.46/bar_2"), true), - (Path::new("test_dirs/1.45"), false), - (Path::new("test_dirs/1.45/foo_2"), true), - (Path::new("test_dirs/1.45/foo_1"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/1.45"), false), - (Path::new("test_dirs/1.45/foo_1"), true), - (Path::new("test_dirs/1.45/foo_2"), true), - (Path::new("test_dirs/1.46"), false), - (Path::new("test_dirs/1.46/bar_1"), true), - (Path::new("test_dirs/1.46/bar_2"), true), - ] - ); - let mut paths = vec![ - (Path::new("root1/one.txt"), true), - (Path::new("root1/one.two.txt"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("root1/one.txt"), true), - (Path::new("root1/one.two.txt"), true), - ] - ); - } - - #[perf] - fn compare_paths_with_same_name_different_extensions() { - let mut paths = vec![ - (Path::new("test_dirs/file.rs"), true), - (Path::new("test_dirs/file.txt"), true), - (Path::new("test_dirs/file.md"), true), - (Path::new("test_dirs/file"), true), - (Path::new("test_dirs/file.a"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("test_dirs/file"), true), - (Path::new("test_dirs/file.a"), true), - (Path::new("test_dirs/file.md"), true), - (Path::new("test_dirs/file.rs"), true), - (Path::new("test_dirs/file.txt"), true), - ] - ); - } - - #[perf] - fn compare_paths_case_semi_sensitive() { - let mut paths = vec![ - (Path::new("test_DIRS"), false), - (Path::new("test_DIRS/foo_1"), true), - (Path::new("test_DIRS/foo_2"), true), - (Path::new("test_DIRS/bar"), true), - (Path::new("test_DIRS/BAR"), true), - (Path::new("test_dirs"), false), - (Path::new("test_dirs/foo_1"), true), - (Path::new("test_dirs/foo_2"), true), - (Path::new("test_dirs/bar"), true), - (Path::new("test_dirs/BAR"), true), - ]; - paths.sort_by(|&a, &b| compare_paths(a, b)); - assert_eq!( - paths, - vec![ - (Path::new("test_dirs"), false), - (Path::new("test_dirs/bar"), true), - (Path::new("test_dirs/BAR"), true), - (Path::new("test_dirs/foo_1"), true), - (Path::new("test_dirs/foo_2"), true), - (Path::new("test_DIRS"), false), - (Path::new("test_DIRS/bar"), true), - (Path::new("test_DIRS/BAR"), true), - (Path::new("test_DIRS/foo_1"), true), - (Path::new("test_DIRS/foo_2"), true), - ] - ); - } - - #[perf] - fn compare_paths_mixed_case_numeric_ordering() { - let mut entries = [ - (Path::new(".config"), false), - (Path::new("Dir1"), false), - (Path::new("dir01"), false), - (Path::new("dir2"), false), - (Path::new("Dir02"), false), - (Path::new("dir10"), false), - (Path::new("Dir10"), false), - ]; - - entries.sort_by(|&a, &b| compare_paths(a, b)); - - let ordered: Vec<&str> = entries - .iter() - .map(|(path, _)| path.to_str().unwrap()) - .collect(); - - assert_eq!( - ordered, - vec![ - ".config", "Dir1", "dir01", "dir2", "Dir02", "dir10", "Dir10" - ] - ); - } - - #[perf] - fn compare_rel_paths_mixed_case_insensitive() { - // Test that mixed mode is case-insensitive - let mut paths = vec![ - (RelPath::unix("zebra.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("Carrot").unwrap(), false), - (RelPath::unix("aardvark.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b)); - // Case-insensitive: aardvark < Apple < banana < Carrot < zebra - assert_eq!( - paths, - vec![ - (RelPath::unix("aardvark.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("Carrot").unwrap(), false), - (RelPath::unix("zebra.txt").unwrap(), true), - ] - ); - } - - #[perf] - fn compare_rel_paths_files_first_basic() { - // Test that files come before directories - let mut paths = vec![ - (RelPath::unix("zebra.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("Carrot").unwrap(), false), - (RelPath::unix("aardvark.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b)); - // Files first (case-insensitive), then directories (case-insensitive) - assert_eq!( - paths, - vec![ - (RelPath::unix("aardvark.txt").unwrap(), true), - (RelPath::unix("banana.rs").unwrap(), true), - (RelPath::unix("zebra.txt").unwrap(), true), - (RelPath::unix("Apple").unwrap(), false), - (RelPath::unix("Carrot").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_files_first_case_insensitive() { - // Test case-insensitive sorting within files and directories - let mut paths = vec![ - (RelPath::unix("Zebra.txt").unwrap(), true), - (RelPath::unix("apple").unwrap(), false), - (RelPath::unix("Banana.rs").unwrap(), true), - (RelPath::unix("carrot").unwrap(), false), - (RelPath::unix("Aardvark.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix("Aardvark.txt").unwrap(), true), - (RelPath::unix("Banana.rs").unwrap(), true), - (RelPath::unix("Zebra.txt").unwrap(), true), - (RelPath::unix("apple").unwrap(), false), - (RelPath::unix("carrot").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_files_first_numeric() { - // Test natural number sorting with files first - let mut paths = vec![ - (RelPath::unix("file10.txt").unwrap(), true), - (RelPath::unix("dir2").unwrap(), false), - (RelPath::unix("file2.txt").unwrap(), true), - (RelPath::unix("dir10").unwrap(), false), - (RelPath::unix("file1.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix("file1.txt").unwrap(), true), - (RelPath::unix("file2.txt").unwrap(), true), - (RelPath::unix("file10.txt").unwrap(), true), - (RelPath::unix("dir2").unwrap(), false), - (RelPath::unix("dir10").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_mixed_case() { - // Test case-insensitive sorting with varied capitalization - let mut paths = vec![ - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("readme.txt").unwrap(), true), - (RelPath::unix("ReadMe.rs").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b)); - // All "readme" variants should group together, sorted by extension - assert_eq!( - paths, - vec![ - (RelPath::unix("readme.txt").unwrap(), true), - (RelPath::unix("ReadMe.rs").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - ] - ); - } - - #[perf] - fn compare_rel_paths_mixed_files_and_dirs() { - // Verify directories and files are still mixed - let mut paths = vec![ - (RelPath::unix("file2.txt").unwrap(), true), - (RelPath::unix("Dir1").unwrap(), false), - (RelPath::unix("file1.txt").unwrap(), true), - (RelPath::unix("dir2").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b)); - // Case-insensitive: dir1, dir2, file1, file2 (all mixed) - assert_eq!( - paths, - vec![ - (RelPath::unix("Dir1").unwrap(), false), - (RelPath::unix("dir2").unwrap(), false), - (RelPath::unix("file1.txt").unwrap(), true), - (RelPath::unix("file2.txt").unwrap(), true), - ] - ); - } - - #[perf] - fn compare_rel_paths_mixed_with_nested_paths() { - // Test that nested paths still work correctly - let mut paths = vec![ - (RelPath::unix("src/main.rs").unwrap(), true), - (RelPath::unix("Cargo.toml").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("target").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix("Cargo.toml").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("src/main.rs").unwrap(), true), - (RelPath::unix("target").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_files_first_with_nested() { - // Files come before directories, even with nested paths - let mut paths = vec![ - (RelPath::unix("src/lib.rs").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("tests").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - (RelPath::unix("src/lib.rs").unwrap(), true), - (RelPath::unix("tests").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_mixed_dotfiles() { - // Test that dotfiles are handled correctly in mixed mode - let mut paths = vec![ - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix("src").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix("src").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_files_first_dotfiles() { - // Test that dotfiles come first when they're files - let mut paths = vec![ - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix("src").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix(".gitignore").unwrap(), true), - (RelPath::unix("README.md").unwrap(), true), - (RelPath::unix(".github").unwrap(), false), - (RelPath::unix("src").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_mixed_same_stem_different_extension() { - // Files with same stem but different extensions should sort by extension - let mut paths = vec![ - (RelPath::unix("file.rs").unwrap(), true), - (RelPath::unix("file.md").unwrap(), true), - (RelPath::unix("file.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix("file.txt").unwrap(), true), - (RelPath::unix("file.rs").unwrap(), true), - (RelPath::unix("file.md").unwrap(), true), - ] - ); - } - - #[perf] - fn compare_rel_paths_files_first_same_stem() { - // Same stem files should still sort by extension with files_first - let mut paths = vec![ - (RelPath::unix("main.rs").unwrap(), true), - (RelPath::unix("main.c").unwrap(), true), - (RelPath::unix("main").unwrap(), false), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_files_first(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix("main.c").unwrap(), true), - (RelPath::unix("main.rs").unwrap(), true), - (RelPath::unix("main").unwrap(), false), - ] - ); - } - - #[perf] - fn compare_rel_paths_mixed_deep_nesting() { - // Test sorting with deeply nested paths - let mut paths = vec![ - (RelPath::unix("a/b/c.txt").unwrap(), true), - (RelPath::unix("A/B.txt").unwrap(), true), - (RelPath::unix("a.txt").unwrap(), true), - (RelPath::unix("A.txt").unwrap(), true), - ]; - paths.sort_by(|&a, &b| compare_rel_paths_mixed(a, b)); - assert_eq!( - paths, - vec![ - (RelPath::unix("A/B.txt").unwrap(), true), - (RelPath::unix("a/b/c.txt").unwrap(), true), - (RelPath::unix("a.txt").unwrap(), true), - (RelPath::unix("A.txt").unwrap(), true), - ] - ); - } - - #[perf] - fn path_with_position_parse_posix_path() { - // Test POSIX filename edge cases - // Read more at https://en.wikipedia.org/wiki/Filename - assert_eq!( - PathWithPosition::parse_str("test_file"), - PathWithPosition { - path: PathBuf::from("test_file"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("a:bc:.zip:1"), - PathWithPosition { - path: PathBuf::from("a:bc:.zip"), - row: Some(1), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("one.second.zip:1"), - PathWithPosition { - path: PathBuf::from("one.second.zip"), - row: Some(1), - column: None - } - ); - - // Trim off trailing `:`s for otherwise valid input. - assert_eq!( - PathWithPosition::parse_str("test_file:10:1:"), - PathWithPosition { - path: PathBuf::from("test_file"), - row: Some(10), - column: Some(1) - } - ); - - assert_eq!( - PathWithPosition::parse_str("test_file.rs:"), - PathWithPosition { - path: PathBuf::from("test_file.rs"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("test_file.rs:1:"), - PathWithPosition { - path: PathBuf::from("test_file.rs"), - row: Some(1), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("ab\ncd"), - PathWithPosition { - path: PathBuf::from("ab\ncd"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("👋\nab"), - PathWithPosition { - path: PathBuf::from("👋\nab"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("Types.hs:(617,9)-(670,28):"), - PathWithPosition { - path: PathBuf::from("Types.hs"), - row: Some(617), - column: Some(9), - } - ); - } - - #[perf] - #[cfg(not(target_os = "windows"))] - fn path_with_position_parse_posix_path_with_suffix() { - assert_eq!( - PathWithPosition::parse_str("foo/bar:34:in"), - PathWithPosition { - path: PathBuf::from("foo/bar"), - row: Some(34), - column: None, - } - ); - assert_eq!( - PathWithPosition::parse_str("foo/bar.rs:1902:::15:"), - PathWithPosition { - path: PathBuf::from("foo/bar.rs:1902"), - row: Some(15), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("app-editors:zed-0.143.6:20240710-201212.log:34:"), - PathWithPosition { - path: PathBuf::from("app-editors:zed-0.143.6:20240710-201212.log"), - row: Some(34), - column: None, - } - ); - - assert_eq!( - PathWithPosition::parse_str("crates/file_finder/src/file_finder.rs:1902:13:"), - PathWithPosition { - path: PathBuf::from("crates/file_finder/src/file_finder.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("crate/utils/src/test:today.log:34"), - PathWithPosition { - path: PathBuf::from("crate/utils/src/test:today.log"), - row: Some(34), - column: None, - } - ); - assert_eq!( - PathWithPosition::parse_str("/testing/out/src/file_finder.odin(7:15)"), - PathWithPosition { - path: PathBuf::from("/testing/out/src/file_finder.odin"), - row: Some(7), - column: Some(15), - } - ); - } - - #[perf] - #[cfg(target_os = "windows")] - fn path_with_position_parse_windows_path() { - assert_eq!( - PathWithPosition::parse_str("crates\\utils\\paths.rs"), - PathWithPosition { - path: PathBuf::from("crates\\utils\\paths.rs"), - row: None, - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: None, - column: None - } - ); - } - - #[perf] - #[cfg(target_os = "windows")] - fn path_with_position_parse_windows_path_with_suffix() { - assert_eq!( - PathWithPosition::parse_str("crates\\utils\\paths.rs:101"), - PathWithPosition { - path: PathBuf::from("crates\\utils\\paths.rs"), - row: Some(101), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1:20"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1), - column: Some(20) - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13)"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13) - } - ); - - // Trim off trailing `:`s for otherwise valid input. - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13) - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:13:15:"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"), - row: Some(13), - column: Some(15) - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs:1902:::15:"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs:1902"), - row: Some(15), - column: None - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902,13):"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("\\\\?\\C:\\Users\\someone\\test_file.rs(1902):"), - PathWithPosition { - path: PathBuf::from("\\\\?\\C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: None, - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs:1902:13:"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902,13):"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: Some(13), - } - ); - - assert_eq!( - PathWithPosition::parse_str("C:\\Users\\someone\\test_file.rs(1902):"), - PathWithPosition { - path: PathBuf::from("C:\\Users\\someone\\test_file.rs"), - row: Some(1902), - column: None, - } - ); - - assert_eq!( - PathWithPosition::parse_str("crates/utils/paths.rs:101"), - PathWithPosition { - path: PathBuf::from("crates\\utils\\paths.rs"), - row: Some(101), - column: None, - } - ); - } - - #[perf] - fn test_path_compact() { - let path: PathBuf = [ - home_dir().to_string_lossy().into_owned(), - "some_file.txt".to_string(), - ] - .iter() - .collect(); - if cfg!(any(target_os = "linux", target_os = "freebsd")) || cfg!(target_os = "macos") { - assert_eq!(path.compact().to_str(), Some("~/some_file.txt")); - } else { - assert_eq!(path.compact().to_str(), path.to_str()); - } - } - - #[perf] - fn test_extension_or_hidden_file_name() { - // No dots in name - let path = Path::new("/a/b/c/file_name.rs"); - assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); - - // Single dot in name - let path = Path::new("/a/b/c/file.name.rs"); - assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); - - // Multiple dots in name - let path = Path::new("/a/b/c/long.file.name.rs"); - assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); - - // Hidden file, no extension - let path = Path::new("/a/b/c/.gitignore"); - assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore")); - - // Hidden file, with extension - let path = Path::new("/a/b/c/.eslintrc.js"); - assert_eq!(path.extension_or_hidden_file_name(), Some("eslintrc.js")); - } - - #[perf] - // fn edge_of_glob() { - // let path = Path::new("/work/node_modules"); - // let path_matcher = - // PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap(); - // assert!( - // path_matcher.is_match(path), - // "Path matcher should match {path:?}" - // ); - // } - - // #[perf] - // fn file_in_dirs() { - // let path = Path::new("/work/.env"); - // let path_matcher = PathMatcher::new(&["**/.env".to_owned()], PathStyle::Posix).unwrap(); - // assert!( - // path_matcher.is_match(path), - // "Path matcher should match {path:?}" - // ); - // let path = Path::new("/work/package.json"); - // assert!( - // !path_matcher.is_match(path), - // "Path matcher should not match {path:?}" - // ); - // } - - // #[perf] - // fn project_search() { - // let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules"); - // let path_matcher = - // PathMatcher::new(&["**/node_modules/**".to_owned()], PathStyle::Posix).unwrap(); - // assert!( - // path_matcher.is_match(path), - // "Path matcher should match {path:?}" - // ); - // } - #[perf] - #[cfg(target_os = "windows")] - fn test_sanitized_path() { - let path = Path::new("C:\\Users\\someone\\test_file.rs"); - let sanitized_path = SanitizedPath::new(path); - assert_eq!( - sanitized_path.to_string(), - "C:\\Users\\someone\\test_file.rs" - ); - - let path = Path::new("\\\\?\\C:\\Users\\someone\\test_file.rs"); - let sanitized_path = SanitizedPath::new(path); - assert_eq!( - sanitized_path.to_string(), - "C:\\Users\\someone\\test_file.rs" - ); - } - - #[perf] - fn test_compare_numeric_segments() { - // Helper function to create peekable iterators and test - fn compare(a: &str, b: &str) -> Ordering { - let mut a_iter = a.chars().peekable(); - let mut b_iter = b.chars().peekable(); - - let result = compare_numeric_segments(&mut a_iter, &mut b_iter); - - // Verify iterators advanced correctly - assert!( - !a_iter.next().is_some_and(|c| c.is_ascii_digit()), - "Iterator a should have consumed all digits" - ); - assert!( - !b_iter.next().is_some_and(|c| c.is_ascii_digit()), - "Iterator b should have consumed all digits" - ); - - result - } - - // Basic numeric comparisons - assert_eq!(compare("0", "0"), Ordering::Equal); - assert_eq!(compare("1", "2"), Ordering::Less); - assert_eq!(compare("9", "10"), Ordering::Less); - assert_eq!(compare("10", "9"), Ordering::Greater); - assert_eq!(compare("99", "100"), Ordering::Less); - - // Leading zeros - assert_eq!(compare("0", "00"), Ordering::Less); - assert_eq!(compare("00", "0"), Ordering::Greater); - assert_eq!(compare("01", "1"), Ordering::Greater); - assert_eq!(compare("001", "1"), Ordering::Greater); - assert_eq!(compare("001", "01"), Ordering::Greater); - - // Same value different representation - assert_eq!(compare("000100", "100"), Ordering::Greater); - assert_eq!(compare("100", "0100"), Ordering::Less); - assert_eq!(compare("0100", "00100"), Ordering::Less); - - // Large numbers - assert_eq!(compare("9999999999", "10000000000"), Ordering::Less); - assert_eq!( - compare( - "340282366920938463463374607431768211455", // u128::MAX - "340282366920938463463374607431768211456" - ), - Ordering::Less - ); - assert_eq!( - compare( - "340282366920938463463374607431768211456", // > u128::MAX - "340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Iterator advancement verification - let mut a_iter = "123abc".chars().peekable(); - let mut b_iter = "456def".chars().peekable(); - - compare_numeric_segments(&mut a_iter, &mut b_iter); - - assert_eq!(a_iter.collect::(), "abc"); - assert_eq!(b_iter.collect::(), "def"); - } - - #[perf] - fn test_natural_sort() { - // Basic alphanumeric - assert_eq!(natural_sort("a", "b"), Ordering::Less); - assert_eq!(natural_sort("b", "a"), Ordering::Greater); - assert_eq!(natural_sort("a", "a"), Ordering::Equal); - - // Case sensitivity - assert_eq!(natural_sort("a", "A"), Ordering::Less); - assert_eq!(natural_sort("A", "a"), Ordering::Greater); - assert_eq!(natural_sort("aA", "aa"), Ordering::Greater); - assert_eq!(natural_sort("aa", "aA"), Ordering::Less); - - // Numbers - assert_eq!(natural_sort("1", "2"), Ordering::Less); - assert_eq!(natural_sort("2", "10"), Ordering::Less); - assert_eq!(natural_sort("02", "10"), Ordering::Less); - assert_eq!(natural_sort("02", "2"), Ordering::Greater); - - // Mixed alphanumeric - assert_eq!(natural_sort("a1", "a2"), Ordering::Less); - assert_eq!(natural_sort("a2", "a10"), Ordering::Less); - assert_eq!(natural_sort("a02", "a2"), Ordering::Greater); - assert_eq!(natural_sort("a1b", "a1c"), Ordering::Less); - - // Multiple numeric segments - assert_eq!(natural_sort("1a2", "1a10"), Ordering::Less); - assert_eq!(natural_sort("1a10", "1a2"), Ordering::Greater); - assert_eq!(natural_sort("2a1", "10a1"), Ordering::Less); - - // Special characters - assert_eq!(natural_sort("a-1", "a-2"), Ordering::Less); - assert_eq!(natural_sort("a_1", "a_2"), Ordering::Less); - assert_eq!(natural_sort("a.1", "a.2"), Ordering::Less); - - // Unicode - assert_eq!(natural_sort("文1", "文2"), Ordering::Less); - assert_eq!(natural_sort("文2", "文10"), Ordering::Less); - assert_eq!(natural_sort("🔤1", "🔤2"), Ordering::Less); - - // Empty and special cases - assert_eq!(natural_sort("", ""), Ordering::Equal); - assert_eq!(natural_sort("", "a"), Ordering::Less); - assert_eq!(natural_sort("a", ""), Ordering::Greater); - assert_eq!(natural_sort(" ", " "), Ordering::Less); - - // Mixed everything - assert_eq!(natural_sort("File-1.txt", "File-2.txt"), Ordering::Less); - assert_eq!(natural_sort("File-02.txt", "File-2.txt"), Ordering::Greater); - assert_eq!(natural_sort("File-2.txt", "File-10.txt"), Ordering::Less); - assert_eq!(natural_sort("File_A1", "File_A2"), Ordering::Less); - assert_eq!(natural_sort("File_a1", "File_A1"), Ordering::Less); - } - - #[perf] - fn test_compare_paths() { - // Helper function for cleaner tests - fn compare(a: &str, is_a_file: bool, b: &str, is_b_file: bool) -> Ordering { - compare_paths((Path::new(a), is_a_file), (Path::new(b), is_b_file)) - } - - // Basic path comparison - assert_eq!(compare("a", true, "b", true), Ordering::Less); - assert_eq!(compare("b", true, "a", true), Ordering::Greater); - assert_eq!(compare("a", true, "a", true), Ordering::Equal); - - // Files vs Directories - assert_eq!(compare("a", true, "a", false), Ordering::Greater); - assert_eq!(compare("a", false, "a", true), Ordering::Less); - assert_eq!(compare("b", false, "a", true), Ordering::Less); - - // Extensions - assert_eq!(compare("a.txt", true, "a.md", true), Ordering::Greater); - assert_eq!(compare("a.md", true, "a.txt", true), Ordering::Less); - assert_eq!(compare("a", true, "a.txt", true), Ordering::Less); - - // Nested paths - assert_eq!(compare("dir/a", true, "dir/b", true), Ordering::Less); - assert_eq!(compare("dir1/a", true, "dir2/a", true), Ordering::Less); - assert_eq!(compare("dir/sub/a", true, "dir/a", true), Ordering::Less); - - // Case sensitivity in paths - assert_eq!( - compare("Dir/file", true, "dir/file", true), - Ordering::Greater - ); - assert_eq!( - compare("dir/File", true, "dir/file", true), - Ordering::Greater - ); - assert_eq!(compare("dir/file", true, "Dir/File", true), Ordering::Less); - - // Hidden files and special names - assert_eq!(compare(".hidden", true, "visible", true), Ordering::Less); - assert_eq!(compare("_special", true, "normal", true), Ordering::Less); - assert_eq!(compare(".config", false, ".data", false), Ordering::Less); - - // Mixed numeric paths - assert_eq!( - compare("dir1/file", true, "dir2/file", true), - Ordering::Less - ); - assert_eq!( - compare("dir2/file", true, "dir10/file", true), - Ordering::Less - ); - assert_eq!( - compare("dir02/file", true, "dir2/file", true), - Ordering::Greater - ); - - // Root paths - assert_eq!(compare("/a", true, "/b", true), Ordering::Less); - assert_eq!(compare("/", false, "/a", true), Ordering::Less); - - // Complex real-world examples - assert_eq!( - compare("project/src/main.rs", true, "project/src/lib.rs", true), - Ordering::Greater - ); - assert_eq!( - compare( - "project/tests/test_1.rs", - true, - "project/tests/test_2.rs", - true - ), - Ordering::Less - ); - assert_eq!( - compare( - "project/v1.0.0/README.md", - true, - "project/v1.10.0/README.md", - true - ), - Ordering::Less - ); - } - - #[perf] - fn test_natural_sort_case_sensitivity() { - std::thread::sleep(std::time::Duration::from_millis(100)); - // Same letter different case - lowercase should come first - assert_eq!(natural_sort("a", "A"), Ordering::Less); - assert_eq!(natural_sort("A", "a"), Ordering::Greater); - assert_eq!(natural_sort("a", "a"), Ordering::Equal); - assert_eq!(natural_sort("A", "A"), Ordering::Equal); - - // Mixed case strings - assert_eq!(natural_sort("aaa", "AAA"), Ordering::Less); - assert_eq!(natural_sort("AAA", "aaa"), Ordering::Greater); - assert_eq!(natural_sort("aAa", "AaA"), Ordering::Less); - - // Different letters - assert_eq!(natural_sort("a", "b"), Ordering::Less); - assert_eq!(natural_sort("A", "b"), Ordering::Less); - assert_eq!(natural_sort("a", "B"), Ordering::Less); - } - - #[perf] - fn test_natural_sort_with_numbers() { - // Basic number ordering - assert_eq!(natural_sort("file1", "file2"), Ordering::Less); - assert_eq!(natural_sort("file2", "file10"), Ordering::Less); - assert_eq!(natural_sort("file10", "file2"), Ordering::Greater); - - // Numbers in different positions - assert_eq!(natural_sort("1file", "2file"), Ordering::Less); - assert_eq!(natural_sort("file1text", "file2text"), Ordering::Less); - assert_eq!(natural_sort("text1file", "text2file"), Ordering::Less); - - // Multiple numbers in string - assert_eq!(natural_sort("file1-2", "file1-10"), Ordering::Less); - assert_eq!(natural_sort("2-1file", "10-1file"), Ordering::Less); - - // Leading zeros - assert_eq!(natural_sort("file002", "file2"), Ordering::Greater); - assert_eq!(natural_sort("file002", "file10"), Ordering::Less); - - // Very large numbers - assert_eq!( - natural_sort("file999999999999999999999", "file999999999999999999998"), - Ordering::Greater - ); - - // u128 edge cases - - // Numbers near u128::MAX (340,282,366,920,938,463,463,374,607,431,768,211,455) - assert_eq!( - natural_sort( - "file340282366920938463463374607431768211454", - "file340282366920938463463374607431768211455" - ), - Ordering::Less - ); - - // Equal length numbers that overflow u128 - assert_eq!( - natural_sort( - "file340282366920938463463374607431768211456", - "file340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Different length numbers that overflow u128 - assert_eq!( - natural_sort( - "file3402823669209384634633746074317682114560", - "file340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Leading zeros with numbers near u128::MAX - assert_eq!( - natural_sort( - "file0340282366920938463463374607431768211455", - "file340282366920938463463374607431768211455" - ), - Ordering::Greater - ); - - // Very large numbers with different lengths (both overflow u128) - assert_eq!( - natural_sort( - "file999999999999999999999999999999999999999999999999", - "file9999999999999999999999999999999999999999999999999" - ), - Ordering::Less - ); - } - - #[perf] - fn test_natural_sort_case_sensitive() { - // Numerically smaller values come first. - assert_eq!(natural_sort("File1", "file2"), Ordering::Less); - assert_eq!(natural_sort("file1", "File2"), Ordering::Less); - - // Numerically equal values: the case-insensitive comparison decides first. - // Case-sensitive comparison only occurs when both are equal case-insensitively. - assert_eq!(natural_sort("Dir1", "dir01"), Ordering::Less); - assert_eq!(natural_sort("dir2", "Dir02"), Ordering::Less); - assert_eq!(natural_sort("dir2", "dir02"), Ordering::Less); - - // Numerically equal and case-insensitively equal: - // the lexicographically smaller (case-sensitive) one wins. - assert_eq!(natural_sort("dir1", "Dir1"), Ordering::Less); - assert_eq!(natural_sort("dir02", "Dir02"), Ordering::Less); - assert_eq!(natural_sort("dir10", "Dir10"), Ordering::Less); - } - - #[perf] - fn test_natural_sort_edge_cases() { - // Empty strings - assert_eq!(natural_sort("", ""), Ordering::Equal); - assert_eq!(natural_sort("", "a"), Ordering::Less); - assert_eq!(natural_sort("a", ""), Ordering::Greater); - - // Special characters - assert_eq!(natural_sort("file-1", "file_1"), Ordering::Less); - assert_eq!(natural_sort("file.1", "file_1"), Ordering::Less); - assert_eq!(natural_sort("file 1", "file_1"), Ordering::Less); - - // Unicode characters - // 9312 vs 9313 - assert_eq!(natural_sort("file①", "file②"), Ordering::Less); - // 9321 vs 9313 - assert_eq!(natural_sort("file⑩", "file②"), Ordering::Greater); - // 28450 vs 23383 - assert_eq!(natural_sort("file漢", "file字"), Ordering::Greater); - - // Mixed alphanumeric with special chars - assert_eq!(natural_sort("file-1a", "file-1b"), Ordering::Less); - assert_eq!(natural_sort("file-1.2", "file-1.10"), Ordering::Less); - assert_eq!(natural_sort("file-1.10", "file-1.2"), Ordering::Greater); - } - - #[test] - fn test_multiple_extensions() { - // No extensions - let path = Path::new("/a/b/c/file_name"); - assert_eq!(path.multiple_extensions(), None); - - // Only one extension - let path = Path::new("/a/b/c/file_name.tsx"); - assert_eq!(path.multiple_extensions(), None); - - // Stories sample extension - let path = Path::new("/a/b/c/file_name.stories.tsx"); - assert_eq!(path.multiple_extensions(), Some("stories.tsx".to_string())); - - // Longer sample extension - let path = Path::new("/a/b/c/long.app.tar.gz"); - assert_eq!(path.multiple_extensions(), Some("app.tar.gz".to_string())); - } - - #[test] - fn test_strip_path_suffix() { - let base = Path::new("/a/b/c/file_name"); - let suffix = Path::new("file_name"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c"))); - - let base = Path::new("/a/b/c/file_name.tsx"); - let suffix = Path::new("file_name.tsx"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b/c"))); - - let base = Path::new("/a/b/c/file_name.stories.tsx"); - let suffix = Path::new("c/file_name.stories.tsx"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a/b"))); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("b/c/long.app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new("/a"))); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("/a/b/c/long.app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), Some(Path::new(""))); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("/a/b/c/no_match.app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), None); - - let base = Path::new("/a/b/c/long.app.tar.gz"); - let suffix = Path::new("app.tar.gz"); - assert_eq!(strip_path_suffix(base, suffix), None); - } - - #[test] - fn test_strip_prefix() { - let expected = [ - ( - PathStyle::Posix, - "/a/b/c", - "/a/b", - Some(rel_path("c").into_arc()), - ), - ( - PathStyle::Posix, - "/a/b/c", - "/a/b/", - Some(rel_path("c").into_arc()), - ), - ( - PathStyle::Posix, - "/a/b/c", - "/", - Some(rel_path("a/b/c").into_arc()), - ), - (PathStyle::Posix, "/a/b/c", "", None), - (PathStyle::Posix, "/a/b//c", "/a/b/", None), - (PathStyle::Posix, "/a/bc", "/a/b", None), - ( - PathStyle::Posix, - "/a/b/c", - "/a/b/c", - Some(rel_path("").into_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b\\c", - "C:\\a\\b", - Some(rel_path("c").into_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b\\c", - "C:\\a\\b\\", - Some(rel_path("c").into_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b\\c", - "C:\\", - Some(rel_path("a/b/c").into_arc()), - ), - (PathStyle::Windows, "C:\\a\\b\\c", "", None), - (PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None), - (PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None), - ( - PathStyle::Windows, - "C:\\a\\b/c", - "C:\\a\\b", - Some(rel_path("c").into_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b/c", - "C:\\a\\b\\", - Some(rel_path("c").into_arc()), - ), - ( - PathStyle::Windows, - "C:\\a\\b/c", - "C:\\a\\b/", - Some(rel_path("c").into_arc()), - ), - ]; - let actual = expected.clone().map(|(style, child, parent, _)| { - ( - style, - child, - parent, - style - .strip_prefix(child.as_ref(), parent.as_ref()) - .map(|rel_path| rel_path.into_arc()), - ) - }); - pretty_assertions::assert_eq!(actual, expected); - } - - #[cfg(target_os = "windows")] - #[test] - fn test_wsl_path() { - use super::WslPath; - let path = "/a/b/c"; - assert_eq!(WslPath::from_path(&path), None); - - let path = r"\\wsl.localhost"; - assert_eq!(WslPath::from_path(&path), None); - - let path = r"\\wsl.localhost\Distro"; - assert_eq!( - WslPath::from_path(&path), - Some(WslPath { - distro: "Distro".to_owned(), - path: "/".into(), - }) - ); - - let path = r"\\wsl.localhost\Distro\blue"; - assert_eq!( - WslPath::from_path(&path), - Some(WslPath { - distro: "Distro".to_owned(), - path: "/blue".into() - }) - ); - - let path = r"\\wsl$\archlinux\tomato\.\paprika\..\aubergine.txt"; - assert_eq!( - WslPath::from_path(&path), - Some(WslPath { - distro: "archlinux".to_owned(), - path: "/tomato/paprika/../aubergine.txt".into() - }) - ); - - let path = r"\\windows.localhost\Distro\foo"; - assert_eq!(WslPath::from_path(&path), None); - } -} diff --git a/crates/util/src/redact.rs b/crates/util/src/redact.rs deleted file mode 100644 index 6b297dfb58..0000000000 --- a/crates/util/src/redact.rs +++ /dev/null @@ -1,15 +0,0 @@ -/// Whether a given environment variable name should have its value redacted -pub fn should_redact(env_var_name: &str) -> bool { - const REDACTED_SUFFIXES: &[&str] = &[ - "KEY", - "TOKEN", - "PASSWORD", - "SECRET", - "PASS", - "CREDENTIALS", - "LICENSE", - ]; - REDACTED_SUFFIXES - .iter() - .any(|suffix| env_var_name.ends_with(suffix)) -} diff --git a/crates/util/src/rel_path.rs b/crates/util/src/rel_path.rs deleted file mode 100644 index 5e20aacad5..0000000000 --- a/crates/util/src/rel_path.rs +++ /dev/null @@ -1,590 +0,0 @@ -use crate::paths::{PathStyle, is_absolute}; -use anyhow::{Context as _, Result, anyhow}; -use serde::{Deserialize, Serialize}; -use std::{ - borrow::{Borrow, Cow}, - fmt, - ops::Deref, - path::{Path, PathBuf}, - sync::Arc, -}; - -/// A file system path that is guaranteed to be relative and normalized. -/// -/// This type can be used to represent paths in a uniform way, regardless of -/// whether they refer to Windows or POSIX file systems, and regardless of -/// the host platform. -/// -/// Internally, paths are stored in POSIX ('/'-delimited) format, but they can -/// be displayed in either POSIX or Windows format. -/// -/// Relative paths are also guaranteed to be valid unicode. -#[repr(transparent)] -#[derive(PartialEq, Eq, Hash, Serialize)] -pub struct RelPath(str); - -/// An owned representation of a file system path that is guaranteed to be -/// relative and normalized. -/// -/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`] -#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct RelPathBuf(String); - -impl RelPath { - /// Creates an empty [`RelPath`]. - pub fn empty() -> &'static Self { - Self::new_unchecked("") - } - - /// Converts a path with a given style into a [`RelPath`]. - /// - /// Returns an error if the path is absolute, or is not valid unicode. - /// - /// This method will normalize the path by removing `.` components, - /// processing `..` components, and removing trailing separators. It does - /// not allocate unless it's necessary to reformat the path. - #[track_caller] - pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result> { - let mut path = path.to_str().context("non utf-8 path")?; - - let (prefixes, suffixes): (&[_], &[_]) = match path_style { - PathStyle::Posix => (&["./"], &['/']), - PathStyle::Windows => (&["./", ".\\"], &['/', '\\']), - }; - - while prefixes.iter().any(|prefix| path.starts_with(prefix)) { - path = &path[prefixes[0].len()..]; - } - while let Some(prefix) = path.strip_suffix(suffixes) - && !prefix.is_empty() - { - path = prefix; - } - - if is_absolute(&path, path_style) { - return Err(anyhow!("absolute path not allowed: {path:?}")); - } - - let mut string = Cow::Borrowed(path); - if path_style == PathStyle::Windows && path.contains('\\') { - string = Cow::Owned(string.as_ref().replace('\\', "/")) - } - - let mut result = match string { - Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)), - Cow::Owned(string) => Cow::Owned(RelPathBuf(string)), - }; - - if result - .components() - .any(|component| component == "" || component == "." || component == "..") - { - let mut normalized = RelPathBuf::new(); - for component in result.components() { - match component { - "" => {} - "." => {} - ".." => { - if !normalized.pop() { - return Err(anyhow!("path is not relative: {result:?}")); - } - } - other => normalized.push(RelPath::new_unchecked(other)), - } - } - result = Cow::Owned(normalized) - } - - Ok(result) - } - - /// Converts a path that is already normalized and uses '/' separators - /// into a [`RelPath`] . - /// - /// Returns an error if the path is not already in the correct format. - #[track_caller] - pub fn unix + ?Sized>(path: &S) -> anyhow::Result<&Self> { - let path = path.as_ref(); - match Self::new(path, PathStyle::Posix)? { - Cow::Borrowed(path) => Ok(path), - Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")), - } - } - - fn new_unchecked(s: &str) -> &Self { - // Safety: `RelPath` is a transparent wrapper around `str`. - unsafe { &*(s as *const str as *const Self) } - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn components(&self) -> RelPathComponents<'_> { - RelPathComponents(&self.0) - } - - pub fn ancestors(&self) -> RelPathAncestors<'_> { - RelPathAncestors(Some(&self.0)) - } - - pub fn file_name(&self) -> Option<&str> { - self.components().next_back() - } - - pub fn file_stem(&self) -> Option<&str> { - Some(self.as_std_path().file_stem()?.to_str().unwrap()) - } - - pub fn extension(&self) -> Option<&str> { - Some(self.as_std_path().extension()?.to_str().unwrap()) - } - - pub fn parent(&self) -> Option<&Self> { - let mut components = self.components(); - components.next_back()?; - Some(components.rest()) - } - - pub fn starts_with(&self, other: &Self) -> bool { - self.strip_prefix(other).is_ok() - } - - pub fn ends_with(&self, other: &Self) -> bool { - if let Some(suffix) = self.0.strip_suffix(&other.0) { - if suffix.ends_with('/') { - return true; - } else if suffix.is_empty() { - return true; - } - } - false - } - - pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> { - if other.is_empty() { - return Ok(self); - } - if let Some(suffix) = self.0.strip_prefix(&other.0) { - if let Some(suffix) = suffix.strip_prefix('/') { - return Ok(Self::new_unchecked(suffix)); - } else if suffix.is_empty() { - return Ok(Self::empty()); - } - } - Err(StripPrefixError) - } - - pub fn len(&self) -> usize { - self.0.matches('/').count() + 1 - } - - pub fn last_n_components(&self, count: usize) -> Option<&Self> { - let len = self.len(); - if len >= count { - let mut components = self.components(); - for _ in 0..(len - count) { - components.next()?; - } - Some(components.rest()) - } else { - None - } - } - - pub fn join(&self, other: &Self) -> Arc { - let result = if self.0.is_empty() { - Cow::Borrowed(&other.0) - } else if other.0.is_empty() { - Cow::Borrowed(&self.0) - } else { - Cow::Owned(format!("{}/{}", &self.0, &other.0)) - }; - Arc::from(Self::new_unchecked(result.as_ref())) - } - - pub fn to_rel_path_buf(&self) -> RelPathBuf { - RelPathBuf(self.0.to_string()) - } - - pub fn into_arc(&self) -> Arc { - Arc::from(self) - } - - /// Convert the path into the wire representation. - pub fn to_proto(&self) -> String { - self.as_unix_str().to_owned() - } - - /// Load the path from its wire representation. - pub fn from_proto(path: &str) -> Result> { - Ok(Arc::from(Self::unix(path)?)) - } - - /// Convert the path into a string with the given path style. - /// - /// Whenever a path is presented to the user, it should be converted to - /// a string via this method. - pub fn display(&self, style: PathStyle) -> Cow<'_, str> { - match style { - PathStyle::Posix => Cow::Borrowed(&self.0), - PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")), - PathStyle::Windows => Cow::Borrowed(&self.0), - } - } - - /// Get the internal unix-style representation of the path. - /// - /// This should not be shown to the user. - pub fn as_unix_str(&self) -> &str { - &self.0 - } - - /// Interprets the path as a [`std::path::Path`], suitable for file system calls. - /// - /// This is guaranteed to be a valid path regardless of the host platform, because - /// the `/` is accepted as a path separator on windows. - /// - /// This should not be shown to the user. - pub fn as_std_path(&self) -> &Path { - Path::new(&self.0) - } -} - -#[derive(Debug)] -pub struct StripPrefixError; - -impl ToOwned for RelPath { - type Owned = RelPathBuf; - - fn to_owned(&self) -> Self::Owned { - self.to_rel_path_buf() - } -} - -impl Borrow for RelPathBuf { - fn borrow(&self) -> &RelPath { - self.as_rel_path() - } -} - -impl PartialOrd for RelPath { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for RelPath { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.components().cmp(other.components()) - } -} - -impl fmt::Debug for RelPath { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl fmt::Debug for RelPathBuf { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl RelPathBuf { - pub fn new() -> Self { - Self(String::new()) - } - - pub fn pop(&mut self) -> bool { - if let Some(ix) = self.0.rfind('/') { - self.0.truncate(ix); - true - } else if !self.is_empty() { - self.0.clear(); - true - } else { - false - } - } - - pub fn push(&mut self, path: &RelPath) { - if !self.is_empty() { - self.0.push('/'); - } - self.0.push_str(&path.0); - } - - pub fn as_rel_path(&self) -> &RelPath { - RelPath::new_unchecked(self.0.as_str()) - } - - pub fn set_extension(&mut self, extension: &str) -> bool { - if let Some(filename) = self.file_name() { - let mut filename = PathBuf::from(filename); - filename.set_extension(extension); - self.pop(); - self.0.push_str(filename.to_str().unwrap()); - true - } else { - false - } - } -} - -impl Into> for RelPathBuf { - fn into(self) -> Arc { - Arc::from(self.as_rel_path()) - } -} - -impl AsRef for RelPathBuf { - fn as_ref(&self) -> &RelPath { - self.as_rel_path() - } -} - -impl AsRef for RelPath { - fn as_ref(&self) -> &RelPath { - self - } -} - -impl Deref for RelPathBuf { - type Target = RelPath; - - fn deref(&self) -> &Self::Target { - self.as_ref() - } -} - -impl<'a> From<&'a RelPath> for Cow<'a, RelPath> { - fn from(value: &'a RelPath) -> Self { - Self::Borrowed(value) - } -} - -impl From<&RelPath> for Arc { - fn from(rel_path: &RelPath) -> Self { - let bytes: Arc = Arc::from(&rel_path.0); - unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) } - } -} - -#[cfg(any(test, feature = "test-support"))] -#[track_caller] -pub fn rel_path(path: &str) -> &RelPath { - RelPath::unix(path).unwrap() -} - -impl PartialEq for RelPath { - fn eq(&self, other: &str) -> bool { - self.0 == *other - } -} - -#[derive(Default)] -pub struct RelPathComponents<'a>(&'a str); - -pub struct RelPathAncestors<'a>(Option<&'a str>); - -const SEPARATOR: char = '/'; - -impl<'a> RelPathComponents<'a> { - pub fn rest(&self) -> &'a RelPath { - RelPath::new_unchecked(self.0) - } -} - -impl<'a> Iterator for RelPathComponents<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - if let Some(sep_ix) = self.0.find(SEPARATOR) { - let (head, tail) = self.0.split_at(sep_ix); - self.0 = &tail[1..]; - Some(head) - } else if self.0.is_empty() { - None - } else { - let result = self.0; - self.0 = ""; - Some(result) - } - } -} - -impl<'a> Iterator for RelPathAncestors<'a> { - type Item = &'a RelPath; - - fn next(&mut self) -> Option { - let result = self.0?; - if let Some(sep_ix) = result.rfind(SEPARATOR) { - self.0 = Some(&result[..sep_ix]); - } else if !result.is_empty() { - self.0 = Some(""); - } else { - self.0 = None; - } - Some(RelPath::new_unchecked(result)) - } -} - -impl<'a> DoubleEndedIterator for RelPathComponents<'a> { - fn next_back(&mut self) -> Option { - if let Some(sep_ix) = self.0.rfind(SEPARATOR) { - let (head, tail) = self.0.split_at(sep_ix); - self.0 = head; - Some(&tail[1..]) - } else if self.0.is_empty() { - None - } else { - let result = self.0; - self.0 = ""; - Some(result) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use itertools::Itertools; - use pretty_assertions::assert_matches; - - #[test] - fn test_rel_path_new() { - assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err()); - assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err()); - assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err()); - - let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); - - let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); - - assert_eq!( - RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local()) - .unwrap() - .as_ref(), - rel_path("foo/baz/quux") - ); - - let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Borrowed(_)); - - let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); - - let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path, rel_path("foo").into()); - assert_matches!(path, Cow::Borrowed(_)); - - let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Owned(_)); - - let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Borrowed(_)); - - let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap(); - assert_eq!(path.as_ref(), rel_path("foo/bar")); - assert_matches!(path, Cow::Owned(_)); - } - - #[test] - fn test_rel_path_components() { - let path = rel_path("foo/bar/baz"); - assert_eq!( - path.components().collect::>(), - vec!["foo", "bar", "baz"] - ); - assert_eq!( - path.components().rev().collect::>(), - vec!["baz", "bar", "foo"] - ); - - let path = rel_path(""); - let mut components = path.components(); - assert_eq!(components.next(), None); - } - - #[test] - fn test_rel_path_ancestors() { - let path = rel_path("foo/bar/baz"); - let mut ancestors = path.ancestors(); - assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz"))); - assert_eq!(ancestors.next(), Some(rel_path("foo/bar"))); - assert_eq!(ancestors.next(), Some(rel_path("foo"))); - assert_eq!(ancestors.next(), Some(rel_path(""))); - assert_eq!(ancestors.next(), None); - - let path = rel_path("foo"); - let mut ancestors = path.ancestors(); - assert_eq!(ancestors.next(), Some(rel_path("foo"))); - assert_eq!(ancestors.next(), Some(RelPath::empty())); - assert_eq!(ancestors.next(), None); - - let path = RelPath::empty(); - let mut ancestors = path.ancestors(); - assert_eq!(ancestors.next(), Some(RelPath::empty())); - assert_eq!(ancestors.next(), None); - } - - #[test] - fn test_rel_path_parent() { - assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar"))); - assert_eq!(rel_path("foo").parent(), Some(RelPath::empty())); - assert_eq!(rel_path("").parent(), None); - } - - #[test] - fn test_rel_path_partial_ord_is_compatible_with_std() { - let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"]; - for [lhs, rhs] in test_cases.iter().array_combinations::<2>() { - assert_eq!( - Path::new(lhs).cmp(Path::new(rhs)), - RelPath::unix(lhs) - .unwrap() - .cmp(&RelPath::unix(rhs).unwrap()) - ); - } - } - - #[test] - fn test_strip_prefix() { - let parent = rel_path(""); - let child = rel_path(".foo"); - - assert!(child.starts_with(parent)); - assert_eq!(child.strip_prefix(parent).unwrap(), child); - } - - #[test] - fn test_rel_path_constructors_absolute_path() { - assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err()); - assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err()); - assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok()); - } - - #[test] - fn test_pop() { - let mut path = rel_path("a/b").to_rel_path_buf(); - path.pop(); - assert_eq!(path.as_rel_path().as_unix_str(), "a"); - path.pop(); - assert_eq!(path.as_rel_path().as_unix_str(), ""); - path.pop(); - assert_eq!(path.as_rel_path().as_unix_str(), ""); - } -} diff --git a/crates/util/src/schemars.rs b/crates/util/src/schemars.rs deleted file mode 100644 index 8124ca8cfe..0000000000 --- a/crates/util/src/schemars.rs +++ /dev/null @@ -1,72 +0,0 @@ -use schemars::{JsonSchema, transform::transform_subschemas}; - -const DEFS_PATH: &str = "#/$defs/"; - -/// Replaces the JSON schema definition for some type if it is in use (in the definitions list), and -/// returns a reference to it. -/// -/// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that -/// there are multiple types that use this name, and unfortunately schemars APIs do not support -/// resolving this ambiguity - see -/// -/// This takes a closure for `schema` because some settings types are not available on the remote -/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry. -pub fn replace_subschema( - generator: &mut schemars::SchemaGenerator, - schema: impl Fn() -> schemars::Schema, -) -> schemars::Schema { - let schema_name = T::schema_name(); - let definitions = generator.definitions_mut(); - assert!(!definitions.contains_key(&format!("{schema_name}2"))); - assert!(definitions.contains_key(schema_name.as_ref())); - definitions.insert(schema_name.to_string(), schema().to_value()); - schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}")) -} - -/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is -/// already in use. -pub fn add_new_subschema( - generator: &mut schemars::SchemaGenerator, - name: &str, - schema: serde_json::Value, -) -> schemars::Schema { - let old_definition = generator.definitions_mut().insert(name.to_string(), schema); - assert_eq!(old_definition, None); - schemars::Schema::new_ref(format!("{DEFS_PATH}{name}")) -} - -/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every -/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used -/// on a map). -#[derive(Clone)] -pub struct DefaultDenyUnknownFields; - -impl schemars::transform::Transform for DefaultDenyUnknownFields { - fn transform(&mut self, schema: &mut schemars::Schema) { - if let Some(object) = schema.as_object_mut() - && object.contains_key("properties") - && !object.contains_key("additionalProperties") - && !object.contains_key("unevaluatedProperties") - { - object.insert("additionalProperties".to_string(), false.into()); - } - transform_subschemas(self, schema); - } -} - -/// Defaults `allowTrailingCommas` to `true`, for use with `json-language-server`. -/// This can be applied to any schema that will be treated as `jsonc`. -/// -/// Note that this is non-recursive and only applied to the root schema. -#[derive(Clone)] -pub struct AllowTrailingCommas; - -impl schemars::transform::Transform for AllowTrailingCommas { - fn transform(&mut self, schema: &mut schemars::Schema) { - if let Some(object) = schema.as_object_mut() - && !object.contains_key("allowTrailingCommas") - { - object.insert("allowTrailingCommas".to_string(), true.into()); - } - } -} diff --git a/crates/util/src/serde.rs b/crates/util/src/serde.rs deleted file mode 100644 index 4aa4bb1a49..0000000000 --- a/crates/util/src/serde.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub const fn default_true() -> bool { - true -} - -pub fn is_default(value: &T) -> bool { - *value == T::default() -} diff --git a/crates/util/src/shell.rs b/crates/util/src/shell.rs deleted file mode 100644 index d51cb39aed..0000000000 --- a/crates/util/src/shell.rs +++ /dev/null @@ -1,980 +0,0 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{borrow::Cow, fmt, path::Path, sync::LazyLock}; - -/// Shell configuration to open the terminal with. -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - /// Use the system's default terminal configuration in /etc/passwd - #[default] - System, - /// Use a specific program with no arguments. - Program(String), - /// Use a specific program with arguments. - WithArguments { - /// The program to run. - program: String, - /// The arguments to pass to the program. - args: Vec, - /// An optional string to override the title of the terminal tab - title_override: Option, - }, -} - -impl Shell { - pub fn program(&self) -> String { - match self { - Shell::Program(program) => program.clone(), - Shell::WithArguments { program, .. } => program.clone(), - Shell::System => get_system_shell(), - } - } - - pub fn program_and_args(&self) -> (String, &[String]) { - match self { - Shell::Program(program) => (program.clone(), &[]), - Shell::WithArguments { program, args, .. } => (program.clone(), args), - Shell::System => (get_system_shell(), &[]), - } - } - - pub fn shell_kind(&self, is_windows: bool) -> ShellKind { - match self { - Shell::Program(program) => ShellKind::new(program, is_windows), - Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows), - Shell::System => ShellKind::system(), - } - } -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum ShellKind { - #[default] - Posix, - Csh, - Tcsh, - Rc, - Fish, - /// Pre-installed "legacy" powershell for windows - PowerShell, - /// PowerShell 7.x - Pwsh, - Nushell, - Cmd, - Xonsh, - Elvish, -} - -pub fn get_system_shell() -> String { - if cfg!(windows) { - get_windows_system_shell() - } else { - std::env::var("SHELL").unwrap_or("/bin/sh".to_string()) - } -} - -pub fn get_default_system_shell() -> String { - if cfg!(windows) { - get_windows_system_shell() - } else { - "/bin/sh".to_string() - } -} - -/// Get the default system shell, preferring bash on Windows. -pub fn get_default_system_shell_preferring_bash() -> String { - if cfg!(windows) { - get_windows_bash().unwrap_or_else(|| get_windows_system_shell()) - } else { - "/bin/sh".to_string() - } -} - -pub fn get_windows_bash() -> Option { - use std::path::PathBuf; - - fn find_bash_in_scoop() -> Option { - let bash_exe = - PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\bash.exe"); - bash_exe.exists().then_some(bash_exe) - } - - fn find_bash_in_git() -> Option { - // /path/to/git/cmd/git.exe/../../bin/bash.exe - let git = which::which("git").ok()?; - let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe"); - git_bash.exists().then_some(git_bash) - } - - static BASH: LazyLock> = LazyLock::new(|| { - let bash = find_bash_in_scoop() - .or_else(|| find_bash_in_git()) - .map(|p| p.to_string_lossy().into_owned()); - if let Some(ref path) = bash { - log::info!("Found bash at {}", path); - } - bash - }); - - (*BASH).clone() -} - -pub fn get_windows_system_shell() -> String { - use std::path::PathBuf; - - fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option { - #[cfg(target_pointer_width = "64")] - let env_var = if find_alternate { - "ProgramFiles(x86)" - } else { - "ProgramFiles" - }; - - #[cfg(target_pointer_width = "32")] - let env_var = if find_alternate { - "ProgramW6432" - } else { - "ProgramFiles" - }; - - let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell"); - install_base_dir - .read_dir() - .ok()? - .filter_map(Result::ok) - .filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir())) - .filter_map(|entry| { - let dir_name = entry.file_name(); - let dir_name = dir_name.to_string_lossy(); - - let version = if find_preview { - let dash_index = dir_name.find('-')?; - if &dir_name[dash_index + 1..] != "preview" { - return None; - }; - dir_name[..dash_index].parse::().ok()? - } else { - dir_name.parse::().ok()? - }; - - let exe_path = entry.path().join("pwsh.exe"); - if exe_path.exists() { - Some((version, exe_path)) - } else { - None - } - }) - .max_by_key(|(version, _)| *version) - .map(|(_, path)| path) - } - - fn find_pwsh_in_msix(find_preview: bool) -> Option { - let msix_app_dir = - PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps"); - if !msix_app_dir.exists() { - return None; - } - - let prefix = if find_preview { - "Microsoft.PowerShellPreview_" - } else { - "Microsoft.PowerShell_" - }; - msix_app_dir - .read_dir() - .ok()? - .filter_map(|entry| { - let entry = entry.ok()?; - if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) { - return None; - } - - if !entry.file_name().to_string_lossy().starts_with(prefix) { - return None; - } - - let exe_path = entry.path().join("pwsh.exe"); - exe_path.exists().then_some(exe_path) - }) - .next() - } - - fn find_pwsh_in_scoop() -> Option { - let pwsh_exe = - PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe"); - pwsh_exe.exists().then_some(pwsh_exe) - } - - static SYSTEM_SHELL: LazyLock = LazyLock::new(|| { - let locations = [ - || find_pwsh_in_programfiles(false, false), - || find_pwsh_in_programfiles(true, false), - || find_pwsh_in_msix(false), - || find_pwsh_in_programfiles(false, true), - || find_pwsh_in_msix(true), - || find_pwsh_in_programfiles(true, true), - || find_pwsh_in_scoop(), - || which::which_global("pwsh.exe").ok(), - || which::which_global("powershell.exe").ok(), - ]; - - locations - .into_iter() - .find_map(|f| f()) - .map(|p| p.to_string_lossy().trim().to_owned()) - .inspect(|shell| log::info!("Found powershell in: {}", shell)) - .unwrap_or_else(|| { - log::warn!("Powershell not found, falling back to `cmd`"); - "cmd.exe".to_string() - }) - }); - - (*SYSTEM_SHELL).clone() -} - -impl fmt::Display for ShellKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ShellKind::Posix => write!(f, "sh"), - ShellKind::Csh => write!(f, "csh"), - ShellKind::Tcsh => write!(f, "tcsh"), - ShellKind::Fish => write!(f, "fish"), - ShellKind::PowerShell => write!(f, "powershell"), - ShellKind::Pwsh => write!(f, "pwsh"), - ShellKind::Nushell => write!(f, "nu"), - ShellKind::Cmd => write!(f, "cmd"), - ShellKind::Rc => write!(f, "rc"), - ShellKind::Xonsh => write!(f, "xonsh"), - ShellKind::Elvish => write!(f, "elvish"), - } - } -} - -impl ShellKind { - pub fn system() -> Self { - Self::new(&get_system_shell(), cfg!(windows)) - } - - pub fn new(program: impl AsRef, is_windows: bool) -> Self { - let program = program.as_ref(); - let program = program - .file_stem() - .unwrap_or_else(|| program.as_os_str()) - .to_string_lossy(); - - match &*program { - "powershell" => ShellKind::PowerShell, - "pwsh" => ShellKind::Pwsh, - "cmd" => ShellKind::Cmd, - "nu" => ShellKind::Nushell, - "fish" => ShellKind::Fish, - "csh" => ShellKind::Csh, - "tcsh" => ShellKind::Tcsh, - "rc" => ShellKind::Rc, - "xonsh" => ShellKind::Xonsh, - "elvish" => ShellKind::Elvish, - "sh" | "bash" | "zsh" => ShellKind::Posix, - _ if is_windows => ShellKind::PowerShell, - // Some other shell detected, the user might install and use a - // unix-like shell. - _ => ShellKind::Posix, - } - } - - pub fn to_shell_variable(self, input: &str) -> String { - match self { - Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input), - Self::Cmd => Self::to_cmd_variable(input), - Self::Posix => input.to_owned(), - Self::Fish => input.to_owned(), - Self::Csh => input.to_owned(), - Self::Tcsh => input.to_owned(), - Self::Rc => input.to_owned(), - Self::Nushell => Self::to_nushell_variable(input), - Self::Xonsh => input.to_owned(), - Self::Elvish => input.to_owned(), - } - } - - fn to_cmd_variable(input: &str) -> String { - if let Some(var_str) = input.strip_prefix("${") { - if var_str.find(':').is_none() { - // If the input starts with "${", remove the trailing "}" - format!("%{}%", &var_str[..var_str.len() - 1]) - } else { - // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, - // which will result in the task failing to run in such cases. - input.into() - } - } else if let Some(var_str) = input.strip_prefix('$') { - // If the input starts with "$", directly append to "$env:" - format!("%{}%", var_str) - } else { - // If no prefix is found, return the input as is - input.into() - } - } - - fn to_powershell_variable(input: &str) -> String { - if let Some(var_str) = input.strip_prefix("${") { - if var_str.find(':').is_none() { - // If the input starts with "${", remove the trailing "}" - format!("$env:{}", &var_str[..var_str.len() - 1]) - } else { - // `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation, - // which will result in the task failing to run in such cases. - input.into() - } - } else if let Some(var_str) = input.strip_prefix('$') { - // If the input starts with "$", directly append to "$env:" - format!("$env:{}", var_str) - } else { - // If no prefix is found, return the input as is - input.into() - } - } - - fn to_nushell_variable(input: &str) -> String { - let mut result = String::new(); - let mut source = input; - let mut is_start = true; - - loop { - match source.chars().next() { - None => return result, - Some('$') => { - source = Self::parse_nushell_var(&source[1..], &mut result, is_start); - is_start = false; - } - Some(_) => { - is_start = false; - let chunk_end = source.find('$').unwrap_or(source.len()); - let (chunk, rest) = source.split_at(chunk_end); - result.push_str(chunk); - source = rest; - } - } - } - } - - fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str { - if source.starts_with("env.") { - text.push('$'); - return source; - } - - match source.chars().next() { - Some('{') => { - let source = &source[1..]; - if let Some(end) = source.find('}') { - let var_name = &source[..end]; - if !var_name.is_empty() { - if !is_start { - text.push_str("("); - } - text.push_str("$env."); - text.push_str(var_name); - if !is_start { - text.push_str(")"); - } - &source[end + 1..] - } else { - text.push_str("${}"); - &source[end + 1..] - } - } else { - text.push_str("${"); - source - } - } - Some(c) if c.is_alphabetic() || c == '_' => { - let end = source - .find(|c: char| !c.is_alphanumeric() && c != '_') - .unwrap_or(source.len()); - let var_name = &source[..end]; - if !is_start { - text.push_str("("); - } - text.push_str("$env."); - text.push_str(var_name); - if !is_start { - text.push_str(")"); - } - &source[end..] - } - _ => { - text.push('$'); - source - } - } - } - - pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec { - match self { - ShellKind::PowerShell | ShellKind::Pwsh => vec!["-C".to_owned(), combined_command], - ShellKind::Cmd => vec![ - "/S".to_owned(), - "/C".to_owned(), - format!("\"{combined_command}\""), - ], - ShellKind::Posix - | ShellKind::Nushell - | ShellKind::Fish - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Xonsh - | ShellKind::Elvish => interactive - .then(|| "-i".to_owned()) - .into_iter() - .chain(["-c".to_owned(), combined_command]) - .collect(), - } - } - - pub const fn command_prefix(&self) -> Option { - match self { - ShellKind::PowerShell | ShellKind::Pwsh => Some('&'), - ShellKind::Nushell => Some('^'), - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Cmd - | ShellKind::Xonsh - | ShellKind::Elvish => None, - } - } - - pub fn prepend_command_prefix<'a>(&self, command: &'a str) -> Cow<'a, str> { - match self.command_prefix() { - Some(prefix) if !command.starts_with(prefix) => { - Cow::Owned(format!("{prefix}{command}")) - } - _ => Cow::Borrowed(command), - } - } - - pub const fn sequential_commands_separator(&self) -> char { - match self { - ShellKind::Cmd => '&', - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Pwsh - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => ';', - } - } - - pub const fn sequential_and_commands_separator(&self) -> &'static str { - match self { - ShellKind::Cmd - | ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Pwsh - | ShellKind::PowerShell - | ShellKind::Xonsh => "&&", - ShellKind::Nushell | ShellKind::Elvish => ";", - } - } - - pub fn try_quote<'a>(&self, arg: &'a str) -> Option> { - match self { - ShellKind::PowerShell => Some(Self::quote_powershell(arg)), - ShellKind::Pwsh => Some(Self::quote_pwsh(arg)), - ShellKind::Cmd => Some(Self::quote_cmd(arg)), - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => shlex::try_quote(arg).ok(), - } - } - - fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> { - if arg.is_empty() { - return Cow::Borrowed("\"\""); - } - - let needs_quoting = arg.chars().any(|c| c == ' ' || c == '\t' || c == '"'); - if !needs_quoting { - return Cow::Borrowed(arg); - } - - let mut result = String::with_capacity(arg.len() + 2); - - if enclose { - result.push('"'); - } - - let chars: Vec = arg.chars().collect(); - let mut i = 0; - - while i < chars.len() { - if chars[i] == '\\' { - let mut num_backslashes = 0; - while i < chars.len() && chars[i] == '\\' { - num_backslashes += 1; - i += 1; - } - - if i < chars.len() && chars[i] == '"' { - // Backslashes followed by quote: double the backslashes and escape the quote - for _ in 0..(num_backslashes * 2 + 1) { - result.push('\\'); - } - result.push('"'); - i += 1; - } else if i >= chars.len() { - // Trailing backslashes: double them (they precede the closing quote) - for _ in 0..(num_backslashes * 2) { - result.push('\\'); - } - } else { - // Backslashes not followed by quote: output as-is - for _ in 0..num_backslashes { - result.push('\\'); - } - } - } else if chars[i] == '"' { - // Quote not preceded by backslash: escape it - result.push('\\'); - result.push('"'); - i += 1; - } else { - result.push(chars[i]); - i += 1; - } - } - - if enclose { - result.push('"'); - } - Cow::Owned(result) - } - - fn needs_quoting_powershell(s: &str) -> bool { - s.is_empty() - || s.chars().any(|c| { - c.is_whitespace() - || matches!( - c, - '"' | '`' - | '$' - | '&' - | '|' - | '<' - | '>' - | ';' - | '(' - | ')' - | '[' - | ']' - | '{' - | '}' - | ',' - | '\'' - | '@' - ) - }) - } - - fn need_quotes_powershell(arg: &str) -> bool { - let mut quote_count = 0; - for c in arg.chars() { - if c == '"' { - quote_count += 1; - } else if c.is_whitespace() && (quote_count % 2 == 0) { - return true; - } - } - false - } - - fn escape_powershell_quotes(s: &str) -> String { - let mut result = String::with_capacity(s.len() + 4); - result.push('\''); - for c in s.chars() { - if c == '\'' { - result.push('\''); - } - result.push(c); - } - result.push('\''); - result - } - - pub fn quote_powershell(arg: &str) -> Cow<'_, str> { - let ps_will_quote = Self::need_quotes_powershell(arg); - let crt_quoted = Self::quote_windows(arg, !ps_will_quote); - - if !Self::needs_quoting_powershell(arg) { - return crt_quoted; - } - - Cow::Owned(Self::escape_powershell_quotes(&crt_quoted)) - } - - pub fn quote_pwsh(arg: &str) -> Cow<'_, str> { - if arg.is_empty() { - return Cow::Borrowed("''"); - } - - if !Self::needs_quoting_powershell(arg) { - return Cow::Borrowed(arg); - } - - Cow::Owned(Self::escape_powershell_quotes(arg)) - } - - pub fn quote_cmd(arg: &str) -> Cow<'_, str> { - let crt_quoted = Self::quote_windows(arg, true); - - let needs_cmd_escaping = crt_quoted.contains('"') - || crt_quoted.contains('%') - || crt_quoted - .chars() - .any(|c| matches!(c, '^' | '<' | '>' | '&' | '|' | '(' | ')')); - - if !needs_cmd_escaping { - return crt_quoted; - } - - let mut result = String::with_capacity(crt_quoted.len() * 2); - for c in crt_quoted.chars() { - match c { - '^' | '"' | '<' | '>' | '&' | '|' | '(' | ')' => { - result.push('^'); - result.push(c); - } - '%' => { - result.push_str("%%cd:~,%"); - } - _ => result.push(c), - } - } - Cow::Owned(result) - } - - /// Quotes the given argument if necessary, taking into account the command prefix. - /// - /// In other words, this will consider quoting arg without its command prefix to not break the command. - /// You should use this over `try_quote` when you want to quote a shell command. - pub fn try_quote_prefix_aware<'a>(&self, arg: &'a str) -> Option> { - if let Some(char) = self.command_prefix() { - if let Some(arg) = arg.strip_prefix(char) { - // we have a command that is prefixed - for quote in ['\'', '"'] { - if let Some(arg) = arg - .strip_prefix(quote) - .and_then(|arg| arg.strip_suffix(quote)) - { - // and the command itself is wrapped as a literal, that - // means the prefix exists to interpret a literal as a - // command. So strip the quotes, quote the command, and - // re-add the quotes if they are missing after requoting - let quoted = self.try_quote(arg)?; - return Some(if quoted.starts_with(['\'', '"']) { - Cow::Owned(self.prepend_command_prefix("ed).into_owned()) - } else { - Cow::Owned( - self.prepend_command_prefix(&format!("{quote}{quoted}{quote}")) - .into_owned(), - ) - }); - } - } - return self - .try_quote(arg) - .map(|quoted| Cow::Owned(self.prepend_command_prefix("ed).into_owned())); - } - } - self.try_quote(arg).map(|quoted| match quoted { - unquoted @ Cow::Borrowed(_) => unquoted, - Cow::Owned(quoted) => Cow::Owned(self.prepend_command_prefix("ed).into_owned()), - }) - } - - pub fn split(&self, input: &str) -> Option> { - shlex::split(input) - } - - pub const fn activate_keyword(&self) -> &'static str { - match self { - ShellKind::Cmd => "", - ShellKind::Nushell => "overlay use", - ShellKind::PowerShell | ShellKind::Pwsh => ".", - ShellKind::Fish - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Posix - | ShellKind::Rc - | ShellKind::Xonsh - | ShellKind::Elvish => "source", - } - } - - pub const fn clear_screen_command(&self) -> &'static str { - match self { - ShellKind::Cmd => "cls", - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Pwsh - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => "clear", - } - } - - #[cfg(windows)] - /// We do not want to escape arguments if we are using CMD as our shell. - /// If we do we end up with too many quotes/escaped quotes for CMD to handle. - pub const fn tty_escape_args(&self) -> bool { - match self { - ShellKind::Cmd => false, - ShellKind::Posix - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Fish - | ShellKind::PowerShell - | ShellKind::Pwsh - | ShellKind::Nushell - | ShellKind::Xonsh - | ShellKind::Elvish => true, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - // Examples - // WSL - // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "echo hello" - // wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "\"echo hello\"" | grep hello" - // wsl.exe --distribution NixOS --cd ~ env RUST_LOG=info,remote=debug .zed_wsl_server/zed-remote-server-dev-build proxy --identifier dev-workspace-53 - // PowerShell from Nushell - // nu -c overlay use "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\activate.nu"; ^"C:\Program Files\PowerShell\7\pwsh.exe" -C "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\python.exe -m pytest \"test_foo.py::test_foo\"" - // PowerShell from CMD - // cmd /C \" \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\activate.bat\"& \"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe\" -C \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"\" - - #[test] - fn test_try_quote_powershell() { - let shell_kind = ShellKind::PowerShell; - assert_eq!( - shell_kind - .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") - .unwrap() - .into_owned(), - "'C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"'".to_string() - ); - } - - #[test] - fn test_try_quote_cmd() { - let shell_kind = ShellKind::Cmd; - assert_eq!( - shell_kind - .try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"") - .unwrap() - .into_owned(), - "^\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\^\"test_foo.py::test_foo\\^\"^\"".to_string() - ); - } - - #[test] - fn test_try_quote_powershell_edge_cases() { - let shell_kind = ShellKind::PowerShell; - - // Empty string - assert_eq!( - shell_kind.try_quote("").unwrap().into_owned(), - "'\"\"'".to_string() - ); - - // String without special characters (no quoting needed) - assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple"); - - // String with spaces - assert_eq!( - shell_kind.try_quote("hello world").unwrap().into_owned(), - "'hello world'".to_string() - ); - - // String with dollar signs - assert_eq!( - shell_kind.try_quote("$variable").unwrap().into_owned(), - "'$variable'".to_string() - ); - - // String with backticks - assert_eq!( - shell_kind.try_quote("test`command").unwrap().into_owned(), - "'test`command'".to_string() - ); - - // String with multiple special characters - assert_eq!( - shell_kind - .try_quote("test `\"$var`\" end") - .unwrap() - .into_owned(), - "'test `\\\"$var`\\\" end'".to_string() - ); - - // String with backslashes and colon (path without spaces doesn't need quoting) - assert_eq!( - shell_kind.try_quote("C:\\path\\to\\file").unwrap(), - "C:\\path\\to\\file" - ); - } - - #[test] - fn test_try_quote_cmd_edge_cases() { - let shell_kind = ShellKind::Cmd; - - // Empty string - assert_eq!( - shell_kind.try_quote("").unwrap().into_owned(), - "^\"^\"".to_string() - ); - - // String without special characters (no quoting needed) - assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple"); - - // String with spaces - assert_eq!( - shell_kind.try_quote("hello world").unwrap().into_owned(), - "^\"hello world^\"".to_string() - ); - - // String with space and backslash (backslash not at end, so not doubled) - assert_eq!( - shell_kind.try_quote("path\\ test").unwrap().into_owned(), - "^\"path\\ test^\"".to_string() - ); - - // String ending with backslash (must be doubled before closing quote) - assert_eq!( - shell_kind.try_quote("test path\\").unwrap().into_owned(), - "^\"test path\\\\^\"".to_string() - ); - - // String ending with multiple backslashes (all doubled before closing quote) - assert_eq!( - shell_kind.try_quote("test path\\\\").unwrap().into_owned(), - "^\"test path\\\\\\\\^\"".to_string() - ); - - // String with embedded quote (quote is escaped, backslash before it is doubled) - assert_eq!( - shell_kind.try_quote("test\\\"quote").unwrap().into_owned(), - "^\"test\\\\\\^\"quote^\"".to_string() - ); - - // String with multiple backslashes before embedded quote (all doubled) - assert_eq!( - shell_kind - .try_quote("test\\\\\"quote") - .unwrap() - .into_owned(), - "^\"test\\\\\\\\\\^\"quote^\"".to_string() - ); - - // String with backslashes not before quotes (path without spaces doesn't need quoting) - assert_eq!( - shell_kind.try_quote("C:\\path\\to\\file").unwrap(), - "C:\\path\\to\\file" - ); - } - - #[test] - fn test_try_quote_nu_command() { - let shell_kind = ShellKind::Nushell; - assert_eq!( - shell_kind.try_quote("'uname'").unwrap().into_owned(), - "\"'uname'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("'uname'") - .unwrap() - .into_owned(), - "^\"'uname'\"".to_string() - ); - assert_eq!( - shell_kind.try_quote("^uname").unwrap().into_owned(), - "'^uname'".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("^uname") - .unwrap() - .into_owned(), - "^uname".to_string() - ); - assert_eq!( - shell_kind.try_quote("^'uname'").unwrap().into_owned(), - "'^'\"'uname\'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("^'uname'") - .unwrap() - .into_owned(), - "^'uname'".to_string() - ); - assert_eq!( - shell_kind.try_quote("'uname a'").unwrap().into_owned(), - "\"'uname a'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("'uname a'") - .unwrap() - .into_owned(), - "^\"'uname a'\"".to_string() - ); - assert_eq!( - shell_kind.try_quote("^'uname a'").unwrap().into_owned(), - "'^'\"'uname a'\"".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("^'uname a'") - .unwrap() - .into_owned(), - "^'uname a'".to_string() - ); - assert_eq!( - shell_kind.try_quote("uname").unwrap().into_owned(), - "uname".to_string() - ); - assert_eq!( - shell_kind - .try_quote_prefix_aware("uname") - .unwrap() - .into_owned(), - "uname".to_string() - ); - } -} diff --git a/crates/util/src/shell_builder.rs b/crates/util/src/shell_builder.rs deleted file mode 100644 index 436c071723..0000000000 --- a/crates/util/src/shell_builder.rs +++ /dev/null @@ -1,298 +0,0 @@ -use std::borrow::Cow; - -use crate::shell::get_system_shell; -use crate::shell::{Shell, ShellKind}; - -/// ShellBuilder is used to turn a user-requested task into a -/// program that can be executed by the shell. -pub struct ShellBuilder { - /// The shell to run - program: String, - args: Vec, - interactive: bool, - /// Whether to redirect stdin to /dev/null for the spawned command as a subshell. - redirect_stdin: bool, - kind: ShellKind, -} - -impl ShellBuilder { - /// Create a new ShellBuilder as configured. - pub fn new(shell: &Shell, is_windows: bool) -> Self { - let (program, args) = match shell { - Shell::System => (get_system_shell(), Vec::new()), - Shell::Program(shell) => (shell.clone(), Vec::new()), - Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), - }; - - let kind = ShellKind::new(&program, is_windows); - Self { - program, - args, - interactive: true, - kind, - redirect_stdin: false, - } - } - pub fn non_interactive(mut self) -> Self { - self.interactive = false; - self - } - - /// Returns the label to show in the terminal tab - pub fn command_label(&self, command_to_use_in_label: &str) -> String { - if command_to_use_in_label.trim().is_empty() { - self.program.clone() - } else { - match self.kind { - ShellKind::PowerShell | ShellKind::Pwsh => { - format!("{} -C '{}'", self.program, command_to_use_in_label) - } - ShellKind::Cmd => { - format!("{} /C \"{}\"", self.program, command_to_use_in_label) - } - ShellKind::Posix - | ShellKind::Nushell - | ShellKind::Fish - | ShellKind::Csh - | ShellKind::Tcsh - | ShellKind::Rc - | ShellKind::Xonsh - | ShellKind::Elvish => { - let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); - format!( - "{PROGRAM} {interactivity}-c '{command_to_use_in_label}'", - PROGRAM = self.program - ) - } - } - } - } - - pub fn redirect_stdin_to_dev_null(mut self) -> Self { - self.redirect_stdin = true; - self - } - - /// Returns the program and arguments to run this task in a shell. - pub fn build( - mut self, - task_command: Option, - task_args: &[String], - ) -> (String, Vec) { - if let Some(task_command) = task_command { - let task_command = if !task_args.is_empty() { - match self.kind.try_quote_prefix_aware(&task_command) { - Some(task_command) => task_command.into_owned(), - None => task_command, - } - } else { - task_command - }; - let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| { - command.push(' '); - let shell_variable = self.kind.to_shell_variable(arg); - command.push_str(&match self.kind.try_quote(&shell_variable) { - Some(shell_variable) => shell_variable, - None => Cow::Owned(shell_variable), - }); - command - }); - if self.redirect_stdin { - match self.kind { - ShellKind::Fish => { - combined_command.insert_str(0, "begin; "); - combined_command.push_str("; end { - combined_command.insert(0, '('); - combined_command.push_str(") { - combined_command.insert_str(0, "$null | & {"); - combined_command.push_str("}"); - } - ShellKind::Cmd => { - combined_command.push_str("< NUL"); - } - } - } - - self.args - .extend(self.kind.args_for_shell(self.interactive, combined_command)); - } - - (self.program, self.args) - } - - // This should not exist, but our task infra is broken beyond repair right now - #[doc(hidden)] - pub fn build_no_quote( - mut self, - task_command: Option, - task_args: &[String], - ) -> (String, Vec) { - if let Some(task_command) = task_command { - let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| { - command.push(' '); - command.push_str(&self.kind.to_shell_variable(arg)); - command - }); - if self.redirect_stdin { - match self.kind { - ShellKind::Fish => { - combined_command.insert_str(0, "begin; "); - combined_command.push_str("; end { - combined_command.insert(0, '('); - combined_command.push_str(") { - combined_command.insert_str(0, "$null | & {"); - combined_command.push_str("}"); - } - ShellKind::Cmd => { - combined_command.push_str("< NUL"); - } - } - } - - self.args - .extend(self.kind.args_for_shell(self.interactive, combined_command)); - } - - (self.program, self.args) - } - - /// Builds a command with the given task command and arguments. - /// - /// Prefer this over manually constructing a command with the output of `Self::build`, - /// as this method handles `cmd` weirdness on windows correctly. - pub fn build_command( - self, - mut task_command: Option, - task_args: &[String], - ) -> smol::process::Command { - #[cfg(windows)] - let kind = self.kind; - if task_args.is_empty() { - task_command = task_command - .as_ref() - .map(|cmd| self.kind.try_quote_prefix_aware(&cmd).map(Cow::into_owned)) - .unwrap_or(task_command); - } - let (program, args) = self.build(task_command, task_args); - - let mut child = crate::command::new_smol_command(program); - - #[cfg(windows)] - if kind == ShellKind::Cmd { - use smol::process::windows::CommandExt; - - for arg in args { - child.raw_arg(arg); - } - } else { - child.args(args); - } - - #[cfg(not(windows))] - child.args(args); - - child - } - - pub fn kind(&self) -> ShellKind { - self.kind - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_nu_shell_variable_substitution() { - let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(&shell, false); - - let (program, args) = shell_builder.build( - Some("echo".into()), - &[ - "${hello}".to_string(), - "$world".to_string(), - "nothing".to_string(), - "--$something".to_string(), - "$".to_string(), - "${test".to_string(), - ], - ); - - assert_eq!(program, "nu"); - assert_eq!( - args, - vec![ - "-i", - "-c", - "echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'" - ] - ); - } - - #[test] - fn redirect_stdin_to_dev_null_precedence() { - let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(&shell, false); - - let (program, args) = shell_builder - .redirect_stdin_to_dev_null() - .build(Some("echo".into()), &["nothing".to_string()]); - - assert_eq!(program, "nu"); - assert_eq!(args, vec!["-i", "-c", "(echo nothing) String { - if use_decimal { - if size < 1000 { - format!("{size}B") - } else if size < 1000 * 1000 { - format!("{:.1}KB", size as f64 / 1000.0) - } else { - format!("{:.1}MB", size as f64 / (1000.0 * 1000.0)) - } - } else if size < 1024 { - format!("{size}B") - } else if size < 1024 * 1024 { - format!("{:.1}KiB", size as f64 / 1024.0) - } else { - format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_file_size_decimal() { - assert_eq!(format_file_size(0, true), "0B"); - assert_eq!(format_file_size(999, true), "999B"); - assert_eq!(format_file_size(1000, true), "1.0KB"); - assert_eq!(format_file_size(1500, true), "1.5KB"); - assert_eq!(format_file_size(999999, true), "1000.0KB"); - assert_eq!(format_file_size(1000000, true), "1.0MB"); - assert_eq!(format_file_size(1500000, true), "1.5MB"); - assert_eq!(format_file_size(10000000, true), "10.0MB"); - } - - #[test] - fn test_format_file_size_binary() { - assert_eq!(format_file_size(0, false), "0B"); - assert_eq!(format_file_size(1023, false), "1023B"); - assert_eq!(format_file_size(1024, false), "1.0KiB"); - assert_eq!(format_file_size(1536, false), "1.5KiB"); - assert_eq!(format_file_size(1048575, false), "1024.0KiB"); - assert_eq!(format_file_size(1048576, false), "1.0MiB"); - assert_eq!(format_file_size(1572864, false), "1.5MiB"); - assert_eq!(format_file_size(10485760, false), "10.0MiB"); - } -} diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs deleted file mode 100644 index 0a251a1e25..0000000000 --- a/crates/util/src/test.rs +++ /dev/null @@ -1,81 +0,0 @@ -mod assertions; -mod marked_text; - -use git2; -use std::{ - ffi::OsStr, - path::{Path, PathBuf}, -}; -use tempfile::TempDir; - -pub use assertions::*; -pub use marked_text::*; - -pub struct TempTree { - _temp_dir: TempDir, - path: PathBuf, -} - -impl TempTree { - pub fn new(tree: serde_json::Value) -> Self { - let dir = TempDir::new().unwrap(); - let path = std::fs::canonicalize(dir.path()).unwrap(); - write_tree(path.as_path(), tree); - - Self { - _temp_dir: dir, - path, - } - } - - pub fn path(&self) -> &Path { - self.path.as_path() - } -} - -fn write_tree(path: &Path, tree: serde_json::Value) { - use serde_json::Value; - use std::fs; - - if let Value::Object(map) = tree { - for (name, contents) in map { - let mut path = PathBuf::from(path); - path.push(name); - match contents { - Value::Object(_) => { - fs::create_dir(&path).unwrap(); - - if path.file_name() == Some(OsStr::new(".git")) { - git2::Repository::init(path.parent().unwrap()).unwrap(); - } - - write_tree(&path, contents); - } - Value::Null => { - fs::create_dir(&path).unwrap(); - } - Value::String(contents) => { - fs::write(&path, contents).unwrap(); - } - _ => { - panic!("JSON object must contain only objects, strings, or null"); - } - } - } - } else { - panic!("You must pass a JSON object to this helper") - } -} - -pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { - let mut text = String::new(); - for row in 0..rows { - let c: char = (start_char as u32 + row as u32) as u8 as char; - let mut line = c.to_string().repeat(cols); - if row < rows - 1 { - line.push('\n'); - } - text += &line; - } - text -} diff --git a/crates/util/src/test/assertions.rs b/crates/util/src/test/assertions.rs deleted file mode 100644 index afb1397fa9..0000000000 --- a/crates/util/src/test/assertions.rs +++ /dev/null @@ -1,62 +0,0 @@ -pub enum SetEqError { - LeftMissing(T), - RightMissing(T), -} - -impl SetEqError { - pub fn map R>(self, update: F) -> SetEqError { - match self { - SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)), - SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)), - } - } -} - -#[macro_export] -macro_rules! set_eq { - ($left:expr,$right:expr) => {{ - use util::test::*; - - let left = $left; - let right = $right; - - let mut result = Ok(()); - for right_value in right.iter() { - if !left.contains(right_value) { - result = Err(SetEqError::LeftMissing(right_value.clone())); - break; - } - } - - if result.is_ok() { - for left_value in left.iter() { - if !right.contains(left_value) { - result = Err(SetEqError::RightMissing(left_value.clone())); - } - } - } - - result - }}; -} - -#[macro_export] -macro_rules! assert_set_eq { - ($left:expr,$right:expr) => {{ - use util::test::*; - use util::set_eq; - - let left = $left; - let right = $right; - - match set_eq!(&left, &right) { - Err(SetEqError::LeftMissing(missing)) => { - panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing); - }, - Err(SetEqError::RightMissing(missing)) => { - panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing); - }, - _ => {} - } - }}; -} diff --git a/crates/util/src/test/marked_text.rs b/crates/util/src/test/marked_text.rs deleted file mode 100644 index 282a477935..0000000000 --- a/crates/util/src/test/marked_text.rs +++ /dev/null @@ -1,281 +0,0 @@ -use collections::HashMap; -use std::{cmp::Ordering, ops::Range}; - -/// Construct a string and a list of offsets within that string using a single -/// string containing embedded position markers. -pub fn marked_text_offsets_by( - marked_text: &str, - markers: Vec, -) -> (String, HashMap>) { - let mut extracted_markers: HashMap> = Default::default(); - let mut unmarked_text = String::new(); - - for char in marked_text.chars() { - if markers.contains(&char) { - let char_offsets = extracted_markers.entry(char).or_default(); - char_offsets.push(unmarked_text.len()); - } else { - unmarked_text.push(char); - } - } - - (unmarked_text, extracted_markers) -} - -/// Construct a string and a list of ranges within that string using a single -/// string containing embedded range markers, using arbitrary characters as -/// range markers. By using multiple different range markers, you can construct -/// ranges that overlap each other. -/// -/// The returned ranges will be grouped by their range marking characters. -pub fn marked_text_ranges_by( - marked_text: &str, - markers: Vec, -) -> (String, HashMap>>) { - let all_markers = markers.iter().flat_map(|m| m.markers()).collect(); - - let (unmarked_text, mut marker_offsets) = marked_text_offsets_by(marked_text, all_markers); - let range_lookup = markers - .into_iter() - .map(|marker| { - ( - marker.clone(), - match marker { - TextRangeMarker::Empty(empty_marker_char) => marker_offsets - .remove(&empty_marker_char) - .unwrap_or_default() - .into_iter() - .map(|empty_index| empty_index..empty_index) - .collect::>>(), - TextRangeMarker::Range(start_marker, end_marker) => { - let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); - let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); - assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); - starts - .into_iter() - .zip(ends) - .map(|(start, end)| { - assert!(end >= start, "marked ranges must be disjoint"); - start..end - }) - .collect::>>() - } - TextRangeMarker::ReverseRange(start_marker, end_marker) => { - let starts = marker_offsets.remove(&start_marker).unwrap_or_default(); - let ends = marker_offsets.remove(&end_marker).unwrap_or_default(); - assert_eq!(starts.len(), ends.len(), "marked ranges are unbalanced"); - starts - .into_iter() - .zip(ends) - .map(|(start, end)| { - assert!(end >= start, "marked ranges must be disjoint"); - end..start - }) - .collect::>>() - } - }, - ) - }) - .collect(); - - (unmarked_text, range_lookup) -} - -/// Construct a string and a list of ranges within that string using a single -/// string containing embedded range markers. The characters used to mark the -/// ranges are as follows: -/// -/// 1. To mark a range of text, surround it with the `«` and `»` angle brackets, -/// which can be typed on a US keyboard with the `alt-|` and `alt-shift-|` keys. -/// -/// ```text -/// foo «selected text» bar -/// ``` -/// -/// 2. To mark a single position in the text, use the `ˇ` caron, -/// which can be typed on a US keyboard with the `alt-shift-t` key. -/// -/// ```text -/// the cursors are hereˇ and hereˇ. -/// ``` -/// -/// 3. To mark a range whose direction is meaningful (like a selection), -/// put a caron character beside one of its bounds, on the inside: -/// -/// ```text -/// one «ˇreversed» selection and one «forwardˇ» selection -/// ``` -/// -/// Any • characters in the input string will be replaced with spaces. This makes -/// it easier to test cases with trailing spaces, which tend to get trimmed from the -/// source code. -#[track_caller] -pub fn marked_text_ranges( - marked_text: &str, - ranges_are_directed: bool, -) -> (String, Vec>) { - let mut unmarked_text = String::with_capacity(marked_text.len()); - let mut ranges = Vec::new(); - let mut prev_marked_ix = 0; - let mut current_range_start = None; - let mut current_range_cursor = None; - - let marked_text = marked_text.replace('•', " "); - for (marked_ix, marker) in marked_text.match_indices(&['«', '»', 'ˇ']) { - unmarked_text.push_str(&marked_text[prev_marked_ix..marked_ix]); - let unmarked_len = unmarked_text.len(); - let len = marker.len(); - prev_marked_ix = marked_ix + len; - - match marker { - "ˇ" => { - if current_range_start.is_some() { - if current_range_cursor.is_some() { - panic!("duplicate point marker 'ˇ' at index {marked_ix}"); - } - - current_range_cursor = Some(unmarked_len); - } else { - ranges.push(unmarked_len..unmarked_len); - } - } - "«" => { - if current_range_start.is_some() { - panic!("unexpected range start marker '«' at index {marked_ix}"); - } - current_range_start = Some(unmarked_len); - } - "»" => { - let current_range_start = if let Some(start) = current_range_start.take() { - start - } else { - panic!("unexpected range end marker '»' at index {marked_ix}"); - }; - - let mut reversed = false; - if let Some(current_range_cursor) = current_range_cursor.take() { - if current_range_cursor == current_range_start { - reversed = true; - } else if current_range_cursor != unmarked_len { - panic!("unexpected 'ˇ' marker in the middle of a range"); - } - } else if ranges_are_directed { - panic!("missing 'ˇ' marker to indicate range direction"); - } - - ranges.push(if reversed { - unmarked_len..current_range_start - } else { - current_range_start..unmarked_len - }); - } - _ => unreachable!(), - } - } - - unmarked_text.push_str(&marked_text[prev_marked_ix..]); - (unmarked_text, ranges) -} - -#[track_caller] -pub fn marked_text_offsets(marked_text: &str) -> (String, Vec) { - let (text, ranges) = marked_text_ranges(marked_text, false); - ( - text, - ranges - .into_iter() - .map(|range| { - assert_eq!(range.start, range.end); - range.start - }) - .collect(), - ) -} - -pub fn generate_marked_text( - unmarked_text: &str, - ranges: &[Range], - indicate_cursors: bool, -) -> String { - let mut marked_text = unmarked_text.to_string(); - for range in ranges.iter().rev() { - if indicate_cursors { - match range.start.cmp(&range.end) { - Ordering::Less => { - marked_text.insert_str(range.end, "ˇ»"); - marked_text.insert(range.start, '«'); - } - Ordering::Equal => { - marked_text.insert(range.start, 'ˇ'); - } - Ordering::Greater => { - marked_text.insert(range.start, '»'); - marked_text.insert_str(range.end, "«ˇ"); - } - } - } else { - match range.start.cmp(&range.end) { - Ordering::Equal => { - marked_text.insert(range.start, 'ˇ'); - } - _ => { - marked_text.insert(range.end, '»'); - marked_text.insert(range.start, '«'); - } - } - } - } - marked_text -} - -#[derive(Clone, Eq, PartialEq, Hash)] -pub enum TextRangeMarker { - Empty(char), - Range(char, char), - ReverseRange(char, char), -} - -impl TextRangeMarker { - fn markers(&self) -> Vec { - match self { - Self::Empty(m) => vec![*m], - Self::Range(l, r) => vec![*l, *r], - Self::ReverseRange(l, r) => vec![*l, *r], - } - } -} - -impl From for TextRangeMarker { - fn from(marker: char) -> Self { - Self::Empty(marker) - } -} - -impl From<(char, char)> for TextRangeMarker { - fn from((left_marker, right_marker): (char, char)) -> Self { - Self::Range(left_marker, right_marker) - } -} - -#[cfg(test)] -mod tests { - use super::{generate_marked_text, marked_text_ranges}; - - #[allow(clippy::reversed_empty_ranges)] - #[test] - fn test_marked_text() { - let (text, ranges) = marked_text_ranges("one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six", true); - - assert_eq!(text, "one two three four five six"); - assert_eq!(ranges.len(), 4); - assert_eq!(ranges[0], 7..4); - assert_eq!(ranges[1], 8..13); - assert_eq!(ranges[2], 18..14); - assert_eq!(ranges[3], 23..23); - - assert_eq!( - generate_marked_text(&text, &ranges, true), - "one «ˇtwo» «threeˇ» «ˇfour» fiveˇ six" - ); - } -} diff --git a/crates/util/src/time.rs b/crates/util/src/time.rs deleted file mode 100644 index 365de6f432..0000000000 --- a/crates/util/src/time.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::time::Duration; - -pub fn duration_alt_display(duration: Duration) -> String { - if duration < Duration::from_secs(60) { - format!("{}s", duration.as_secs()) - } else { - duration_clock_format(duration) - } -} - -fn duration_clock_format(duration: Duration) -> String { - let hours = duration.as_secs() / 3600; - let minutes = (duration.as_secs() % 3600) / 60; - let seconds = duration.as_secs() % 60; - - if hours > 0 { - format!("{hours}:{minutes:02}:{seconds:02}") - } else if minutes > 0 { - format!("{minutes}:{seconds:02}") - } else { - format!("{seconds}") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_duration_to_clock_format() { - use duration_clock_format as f; - assert_eq!("0", f(Duration::from_secs(0))); - assert_eq!("59", f(Duration::from_secs(59))); - assert_eq!("1:00", f(Duration::from_secs(60))); - assert_eq!("10:00", f(Duration::from_secs(600))); - assert_eq!("1:00:00", f(Duration::from_secs(3600))); - assert_eq!("3:02:01", f(Duration::from_secs(3600 * 3 + 60 * 2 + 1))); - assert_eq!("23:59:59", f(Duration::from_secs(3600 * 24 - 1))); - assert_eq!("100:00:00", f(Duration::from_secs(3600 * 100))); - } -} diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs deleted file mode 100644 index d7383a4975..0000000000 --- a/crates/util/src/util.rs +++ /dev/null @@ -1,1175 +0,0 @@ -pub mod arc_cow; -pub mod archive; -pub mod command; -pub mod fs; -pub mod markdown; -pub mod paths; -pub mod redact; -pub mod rel_path; -pub mod schemars; -pub mod serde; -pub mod shell; -pub mod shell_builder; -pub mod size; -#[cfg(any(test, feature = "test-support"))] -pub mod test; -pub mod time; - -use anyhow::Result; -use futures::Future; -use itertools::Either; - -use regex::Regex; - -use std::sync::{LazyLock, OnceLock}; -use std::{ - borrow::Cow, - cmp::{self, Ordering}, - env, - ops::{AddAssign, Range, RangeInclusive}, - panic::Location, - pin::Pin, - task::{Context, Poll}, - time::Instant, -}; -use unicase::UniCase; - -pub use take_until::*; -#[cfg(any(test, feature = "test-support"))] -pub use util_macros::{line_endings, path, uri}; - -#[macro_export] -macro_rules! debug_panic { - ( $($fmt_arg:tt)* ) => { - if cfg!(debug_assertions) { - panic!( $($fmt_arg)* ); - } else { - let backtrace = std::backtrace::Backtrace::capture(); - log::error!("{}\n{:?}", format_args!($($fmt_arg)*), backtrace); - } - }; -} - -#[inline] -pub const fn is_utf8_char_boundary(u8: u8) -> bool { - // This is bit magic equivalent to: b < 128 || b >= 192 - (u8 as i8) >= -0x40 -} - -pub fn truncate(s: &str, max_chars: usize) -> &str { - match s.char_indices().nth(max_chars) { - None => s, - Some((idx, _)) => &s[..idx], - } -} - -/// Removes characters from the end of the string if its length is greater than `max_chars` and -/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars. -pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { - debug_assert!(max_chars >= 5); - - // If the string's byte length is <= max_chars, walking the string can be skipped since the - // number of chars is <= the number of bytes. - if s.len() <= max_chars { - return s.to_string(); - } - let truncation_ix = s.char_indices().map(|(i, _)| i).nth(max_chars); - match truncation_ix { - Some(index) => s[..index].to_string() + "…", - _ => s.to_string(), - } -} - -/// Removes characters from the front of the string if its length is greater than `max_chars` and -/// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars. -pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String { - debug_assert!(max_chars >= 5); - - // If the string's byte length is <= max_chars, walking the string can be skipped since the - // number of chars is <= the number of bytes. - if s.len() <= max_chars { - return s.to_string(); - } - let suffix_char_length = max_chars.saturating_sub(1); - let truncation_ix = s - .char_indices() - .map(|(i, _)| i) - .nth_back(suffix_char_length); - match truncation_ix { - Some(index) if index > 0 => "…".to_string() + &s[index..], - _ => s.to_string(), - } -} - -/// Takes only `max_lines` from the string and, if there were more than `max_lines-1`, appends a -/// a newline and "..." to the string, so that `max_lines` are returned. -/// Returns string unchanged if its length is smaller than max_lines. -pub fn truncate_lines_and_trailoff(s: &str, max_lines: usize) -> String { - let mut lines = s.lines().take(max_lines).collect::>(); - if lines.len() > max_lines - 1 { - lines.pop(); - lines.join("\n") + "\n…" - } else { - lines.join("\n") - } -} - -/// Truncates the string at a character boundary, such that the result is less than `max_bytes` in -/// length. -pub fn truncate_to_byte_limit(s: &str, max_bytes: usize) -> &str { - if s.len() < max_bytes { - return s; - } - - for i in (0..max_bytes).rev() { - if s.is_char_boundary(i) { - return &s[..i]; - } - } - - "" -} - -/// Takes a prefix of complete lines which fit within the byte limit. If the first line is longer -/// than the limit, truncates at a character boundary. -pub fn truncate_lines_to_byte_limit(s: &str, max_bytes: usize) -> &str { - if s.len() < max_bytes { - return s; - } - - for i in (0..max_bytes).rev() { - if s.is_char_boundary(i) && s.as_bytes()[i] == b'\n' { - // Since the i-th character is \n, valid to slice at i + 1. - return &s[..i + 1]; - } - } - - truncate_to_byte_limit(s, max_bytes) -} - -#[test] -fn test_truncate_lines_to_byte_limit() { - let text = "Line 1\nLine 2\nLine 3\nLine 4"; - - // Limit that includes all lines - assert_eq!(truncate_lines_to_byte_limit(text, 100), text); - - // Exactly the first line - assert_eq!(truncate_lines_to_byte_limit(text, 7), "Line 1\n"); - - // Limit between lines - assert_eq!(truncate_lines_to_byte_limit(text, 13), "Line 1\n"); - assert_eq!(truncate_lines_to_byte_limit(text, 20), "Line 1\nLine 2\n"); - - // Limit before first newline - assert_eq!(truncate_lines_to_byte_limit(text, 6), "Line "); - - // Test with non-ASCII characters - let text_utf8 = "Line 1\nLíne 2\nLine 3"; - assert_eq!( - truncate_lines_to_byte_limit(text_utf8, 15), - "Line 1\nLíne 2\n" - ); -} - -pub fn post_inc + AddAssign + Copy>(value: &mut T) -> T { - let prev = *value; - *value += T::from(1); - prev -} - -/// Extend a sorted vector with a sorted sequence of items, maintaining the vector's sort order and -/// enforcing a maximum length. This also de-duplicates items. Sort the items according to the given callback. Before calling this, -/// both `vec` and `new_items` should already be sorted according to the `cmp` comparator. -pub fn extend_sorted(vec: &mut Vec, new_items: I, limit: usize, mut cmp: F) -where - I: IntoIterator, - F: FnMut(&T, &T) -> Ordering, -{ - let mut start_index = 0; - for new_item in new_items { - if let Err(i) = vec[start_index..].binary_search_by(|m| cmp(m, &new_item)) { - let index = start_index + i; - if vec.len() < limit { - vec.insert(index, new_item); - } else if index < vec.len() { - vec.pop(); - vec.insert(index, new_item); - } - start_index = index; - } - } -} - -pub fn truncate_to_bottom_n_sorted_by(items: &mut Vec, limit: usize, compare: &F) -where - F: Fn(&T, &T) -> Ordering, -{ - if limit == 0 { - items.truncate(0); - } - if items.len() <= limit { - items.sort_by(compare); - return; - } - // When limit is near to items.len() it may be more efficient to sort the whole list and - // truncate, rather than always doing selection first as is done below. It's hard to analyze - // where the threshold for this should be since the quickselect style algorithm used by - // `select_nth_unstable_by` makes the prefix partially sorted, and so its work is not wasted - - // the expected number of comparisons needed by `sort_by` is less than it is for some arbitrary - // unsorted input. - items.select_nth_unstable_by(limit, compare); - items.truncate(limit); - items.sort_by(compare); -} - - - -/// Configures the process to start a new session, to prevent interactive shells from taking control -/// of the terminal. -/// -/// For more details: -pub fn set_pre_exec_to_start_new_session( - command: &mut std::process::Command, -) -> &mut std::process::Command { - // safety: code in pre_exec should be signal safe. - // https://man7.org/linux/man-pages/man7/signal-safety.7.html - #[cfg(not(target_os = "windows"))] - unsafe { - use std::os::unix::process::CommandExt; - command.pre_exec(|| { - libc::setsid(); - #[cfg(target_os = "macos")] - crate::command::reset_exception_ports(); - Ok(()) - }); - }; - command -} - -pub fn merge_json_lenient_value_into( - source: serde_json_lenient::Value, - target: &mut serde_json_lenient::Value, -) { - match (source, target) { - (serde_json_lenient::Value::Object(source), serde_json_lenient::Value::Object(target)) => { - for (key, value) in source { - if let Some(target) = target.get_mut(&key) { - merge_json_lenient_value_into(value, target); - } else { - target.insert(key, value); - } - } - } - - (serde_json_lenient::Value::Array(source), serde_json_lenient::Value::Array(target)) => { - for value in source { - target.push(value); - } - } - - (source, target) => *target = source, - } -} - -pub fn merge_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { - use serde_json::Value; - - match (source, target) { - (Value::Object(source), Value::Object(target)) => { - for (key, value) in source { - if let Some(target) = target.get_mut(&key) { - merge_json_value_into(value, target); - } else { - target.insert(key, value); - } - } - } - - (Value::Array(source), Value::Array(target)) => { - for value in source { - target.push(value); - } - } - - (source, target) => *target = source, - } -} - -pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut serde_json::Value) { - use serde_json::Value; - if let Value::Object(source_object) = source { - let target_object = if let Value::Object(target) = target { - target - } else { - *target = Value::Object(Default::default()); - target.as_object_mut().unwrap() - }; - for (key, value) in source_object { - if let Some(target) = target_object.get_mut(&key) { - merge_non_null_json_value_into(value, target); - } else if !value.is_null() { - target_object.insert(key, value); - } - } - } else if !source.is_null() { - *target = source - } -} - -pub fn measure(label: &str, f: impl FnOnce() -> R) -> R { - static ZED_MEASUREMENTS: OnceLock = OnceLock::new(); - let zed_measurements = ZED_MEASUREMENTS.get_or_init(|| { - env::var("ZED_MEASUREMENTS") - .map(|measurements| measurements == "1" || measurements == "true") - .unwrap_or(false) - }); - - if *zed_measurements { - let start = Instant::now(); - let result = f(); - let elapsed = start.elapsed(); - eprintln!("{}: {:?}", label, elapsed); - result - } else { - f() - } -} - -pub fn expanded_and_wrapped_usize_range( - range: Range, - additional_before: usize, - additional_after: usize, - wrap_length: usize, -) -> impl Iterator { - let start_wraps = range.start < additional_before; - let end_wraps = wrap_length < range.end + additional_after; - if start_wraps && end_wraps { - Either::Left(0..wrap_length) - } else if start_wraps { - let wrapped_start = (range.start + wrap_length).saturating_sub(additional_before); - if wrapped_start <= range.end { - Either::Left(0..wrap_length) - } else { - Either::Right((0..range.end + additional_after).chain(wrapped_start..wrap_length)) - } - } else if end_wraps { - let wrapped_end = range.end + additional_after - wrap_length; - if range.start <= wrapped_end { - Either::Left(0..wrap_length) - } else { - Either::Right((0..wrapped_end).chain(range.start - additional_before..wrap_length)) - } - } else { - Either::Left((range.start - additional_before)..(range.end + additional_after)) - } -} - -/// Yields `[i, i + 1, i - 1, i + 2, ..]`, each modulo `wrap_length` and bounded by -/// `additional_before` and `additional_after`. If the wrapping causes overlap, duplicates are not -/// emitted. If wrap_length is 0, nothing is yielded. -pub fn wrapped_usize_outward_from( - start: usize, - additional_before: usize, - additional_after: usize, - wrap_length: usize, -) -> impl Iterator { - let mut count = 0; - let mut after_offset = 1; - let mut before_offset = 1; - - std::iter::from_fn(move || { - count += 1; - if count > wrap_length { - None - } else if count == 1 { - Some(start % wrap_length) - } else if after_offset <= additional_after && after_offset <= before_offset { - let value = (start + after_offset) % wrap_length; - after_offset += 1; - Some(value) - } else if before_offset <= additional_before { - let value = (start + wrap_length - before_offset) % wrap_length; - before_offset += 1; - Some(value) - } else if after_offset <= additional_after { - let value = (start + after_offset) % wrap_length; - after_offset += 1; - Some(value) - } else { - None - } - }) -} - -pub trait ResultExt { - type Ok; - - fn log_err(self) -> Option; - /// Assert that this result should never be an error in development or tests. - fn debug_assert_ok(self, reason: &str) -> Self; - fn warn_on_err(self) -> Option; - fn log_with_level(self, level: log::Level) -> Option; - fn anyhow(self) -> anyhow::Result - where - E: Into; -} - -impl ResultExt for Result -where - E: std::fmt::Debug, -{ - type Ok = T; - - #[track_caller] - fn log_err(self) -> Option { - self.log_with_level(log::Level::Error) - } - - #[track_caller] - fn debug_assert_ok(self, reason: &str) -> Self { - if let Err(error) = &self { - debug_panic!("{reason} - {error:?}"); - } - self - } - - #[track_caller] - fn warn_on_err(self) -> Option { - self.log_with_level(log::Level::Warn) - } - - #[track_caller] - fn log_with_level(self, level: log::Level) -> Option { - match self { - Ok(value) => Some(value), - Err(error) => { - log_error_with_caller(*Location::caller(), error, level); - None - } - } - } - - fn anyhow(self) -> anyhow::Result - where - E: Into, - { - self.map_err(Into::into) - } -} - -fn log_error_with_caller(caller: core::panic::Location<'_>, error: E, level: log::Level) -where - E: std::fmt::Debug, -{ - #[cfg(not(target_os = "windows"))] - let file = caller.file(); - #[cfg(target_os = "windows")] - let file = caller.file().replace('\\', "/"); - // In this codebase all crates reside in a `crates` directory, - // so discard the prefix up to that segment to find the crate name - let file = file.split_once("crates/"); - let target = file.as_ref().and_then(|(_, s)| s.split_once("/src/")); - - let module_path = target.map(|(krate, module)| { - if module.starts_with(krate) { - module.trim_end_matches(".rs").replace('/', "::") - } else { - krate.to_owned() + "::" + &module.trim_end_matches(".rs").replace('/', "::") - } - }); - let file = file.map(|(_, file)| format!("crates/{file}")); - log::logger().log( - &log::Record::builder() - .target(module_path.as_deref().unwrap_or("")) - .module_path(file.as_deref()) - .args(format_args!("{:?}", error)) - .file(Some(caller.file())) - .line(Some(caller.line())) - .level(level) - .build(), - ); -} - -pub fn log_err(error: &E) { - log_error_with_caller(*Location::caller(), error, log::Level::Error); -} - -pub trait TryFutureExt { - fn log_err(self) -> LogErrorFuture - where - Self: Sized; - - fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture - where - Self: Sized; - - fn warn_on_err(self) -> LogErrorFuture - where - Self: Sized; - fn unwrap(self) -> UnwrapFuture - where - Self: Sized; -} - -impl TryFutureExt for F -where - F: Future>, - E: std::fmt::Debug, -{ - #[track_caller] - fn log_err(self) -> LogErrorFuture - where - Self: Sized, - { - let location = Location::caller(); - LogErrorFuture(self, log::Level::Error, *location) - } - - fn log_tracked_err(self, location: core::panic::Location<'static>) -> LogErrorFuture - where - Self: Sized, - { - LogErrorFuture(self, log::Level::Error, location) - } - - #[track_caller] - fn warn_on_err(self) -> LogErrorFuture - where - Self: Sized, - { - let location = Location::caller(); - LogErrorFuture(self, log::Level::Warn, *location) - } - - fn unwrap(self) -> UnwrapFuture - where - Self: Sized, - { - UnwrapFuture(self) - } -} - -#[must_use] -pub struct LogErrorFuture(F, log::Level, core::panic::Location<'static>); - -impl Future for LogErrorFuture -where - F: Future>, - E: std::fmt::Debug, -{ - type Output = Option; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let level = self.1; - let location = self.2; - let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; - match inner.poll(cx) { - Poll::Ready(output) => Poll::Ready(match output { - Ok(output) => Some(output), - Err(error) => { - log_error_with_caller(location, error, level); - None - } - }), - Poll::Pending => Poll::Pending, - } - } -} - -pub struct UnwrapFuture(F); - -impl Future for UnwrapFuture -where - F: Future>, - E: std::fmt::Debug, -{ - type Output = T; - - fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let inner = unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().0) }; - match inner.poll(cx) { - Poll::Ready(result) => Poll::Ready(result.unwrap()), - Poll::Pending => Poll::Pending, - } - } -} - -pub struct Deferred(Option); - -impl Deferred { - /// Drop without running the deferred function. - pub fn abort(mut self) { - self.0.take(); - } -} - -impl Drop for Deferred { - fn drop(&mut self) { - if let Some(f) = self.0.take() { - f() - } - } -} - -/// Run the given function when the returned value is dropped (unless it's cancelled). -#[must_use] -pub fn defer(f: F) -> Deferred { - Deferred(Some(f)) -} - -#[cfg(any(test, feature = "test-support"))] -mod rng { - use rand::prelude::*; - - pub struct RandomCharIter { - rng: T, - simple_text: bool, - } - - impl RandomCharIter { - pub fn new(rng: T) -> Self { - Self { - rng, - simple_text: std::env::var("SIMPLE_TEXT").is_ok_and(|v| !v.is_empty()), - } - } - - pub fn with_simple_text(mut self) -> Self { - self.simple_text = true; - self - } - } - - impl Iterator for RandomCharIter { - type Item = char; - - fn next(&mut self) -> Option { - if self.simple_text { - return if self.rng.random_range(0..100) < 5 { - Some('\n') - } else { - Some(self.rng.random_range(b'a'..b'z' + 1).into()) - }; - } - - match self.rng.random_range(0..100) { - // whitespace - 0..=19 => [' ', '\n', '\r', '\t'].choose(&mut self.rng).copied(), - // two-byte greek letters - 20..=32 => char::from_u32(self.rng.random_range(('α' as u32)..('ω' as u32 + 1))), - // // three-byte characters - 33..=45 => ['✋', '✅', '❌', '❎', '⭐'] - .choose(&mut self.rng) - .copied(), - // // four-byte characters - 46..=58 => ['🍐', '🏀', '🍗', '🎉'].choose(&mut self.rng).copied(), - // ascii letters - _ => Some(self.rng.random_range(b'a'..b'z' + 1).into()), - } - } - } -} -#[cfg(any(test, feature = "test-support"))] -pub use rng::RandomCharIter; - -/// Get an embedded file as a string. -pub fn asset_str(path: &str) -> Cow<'static, str> { - match A::get(path).expect(path).data { - Cow::Borrowed(bytes) => Cow::Borrowed(std::str::from_utf8(bytes).unwrap()), - Cow::Owned(bytes) => Cow::Owned(String::from_utf8(bytes).unwrap()), - } -} - -/// Expands to an immediately-invoked function expression. Good for using the ? operator -/// in functions which do not return an Option or Result. -/// -/// Accepts a normal block, an async block, or an async move block. -#[macro_export] -macro_rules! maybe { - ($block:block) => { - (|| $block)() - }; - (async $block:block) => { - (async || $block)() - }; - (async move $block:block) => { - (async move || $block)() - }; -} - -pub trait RangeExt { - fn sorted(&self) -> Self; - fn to_inclusive(&self) -> RangeInclusive; - fn overlaps(&self, other: &Range) -> bool; - fn contains_inclusive(&self, other: &Range) -> bool; -} - -impl RangeExt for Range { - fn sorted(&self) -> Self { - cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone() - } - - fn to_inclusive(&self) -> RangeInclusive { - self.start.clone()..=self.end.clone() - } - - fn overlaps(&self, other: &Range) -> bool { - self.start < other.end && other.start < self.end - } - - fn contains_inclusive(&self, other: &Range) -> bool { - self.start <= other.start && other.end <= self.end - } -} - -impl RangeExt for RangeInclusive { - fn sorted(&self) -> Self { - cmp::min(self.start(), self.end()).clone()..=cmp::max(self.start(), self.end()).clone() - } - - fn to_inclusive(&self) -> RangeInclusive { - self.clone() - } - - fn overlaps(&self, other: &Range) -> bool { - self.start() < &other.end && &other.start <= self.end() - } - - fn contains_inclusive(&self, other: &Range) -> bool { - self.start() <= &other.start && &other.end <= self.end() - } -} - -/// A way to sort strings with starting numbers numerically first, falling back to alphanumeric one, -/// case-insensitive. -/// -/// This is useful for turning regular alphanumerically sorted sequences as `1-abc, 10, 11-def, .., 2, 21-abc` -/// into `1-abc, 2, 10, 11-def, .., 21-abc` -#[derive(Debug, PartialEq, Eq)] -pub struct NumericPrefixWithSuffix<'a>(Option, &'a str); - -impl<'a> NumericPrefixWithSuffix<'a> { - pub fn from_numeric_prefixed_str(str: &'a str) -> Self { - let i = str.chars().take_while(|c| c.is_ascii_digit()).count(); - let (prefix, remainder) = str.split_at(i); - - let prefix = prefix.parse().ok(); - Self(prefix, remainder) - } -} - -/// When dealing with equality, we need to consider the case of the strings to achieve strict equality -/// to handle cases like "a" < "A" instead of "a" == "A". -impl Ord for NumericPrefixWithSuffix<'_> { - fn cmp(&self, other: &Self) -> Ordering { - match (self.0, other.0) { - (None, None) => UniCase::new(self.1) - .cmp(&UniCase::new(other.1)) - .then_with(|| self.1.cmp(other.1).reverse()), - (None, Some(_)) => Ordering::Greater, - (Some(_), None) => Ordering::Less, - (Some(a), Some(b)) => a.cmp(&b).then_with(|| { - UniCase::new(self.1) - .cmp(&UniCase::new(other.1)) - .then_with(|| self.1.cmp(other.1).reverse()) - }), - } - } -} - -impl PartialOrd for NumericPrefixWithSuffix<'_> { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// Capitalizes the first character of a string. -/// -/// This function takes a string slice as input and returns a new `String` with the first character -/// capitalized. -/// -/// # Examples -/// -/// ``` -/// use util::capitalize; -/// -/// assert_eq!(capitalize("hello"), "Hello"); -/// assert_eq!(capitalize("WORLD"), "WORLD"); -/// assert_eq!(capitalize(""), ""); -/// ``` -pub fn capitalize(str: &str) -> String { - let mut chars = str.chars(); - match chars.next() { - None => String::new(), - Some(first_char) => first_char.to_uppercase().collect::() + chars.as_str(), - } -} - -fn emoji_regex() -> &'static Regex { - static EMOJI_REGEX: LazyLock = - LazyLock::new(|| Regex::new("(\\p{Emoji}|\u{200D})").unwrap()); - &EMOJI_REGEX -} - -/// Returns true if the given string consists of emojis only. -/// E.g. "👨‍👩‍👧‍👧👋" will return true, but "👋!" will return false. -pub fn word_consists_of_emojis(s: &str) -> bool { - let mut prev_end = 0; - for capture in emoji_regex().find_iter(s) { - if capture.start() != prev_end { - return false; - } - prev_end = capture.end(); - } - prev_end == s.len() -} - -/// Similar to `str::split`, but also provides byte-offset ranges of the results. Unlike -/// `str::split`, this is not generic on pattern types and does not return an `Iterator`. -pub fn split_str_with_ranges(s: &str, pat: impl Fn(char) -> bool) -> Vec<(Range, &str)> { - let mut result = Vec::new(); - let mut start = 0; - - for (i, ch) in s.char_indices() { - if pat(ch) { - if i > start { - result.push((start..i, &s[start..i])); - } - start = i + ch.len_utf8(); - } - } - - if s.len() > start { - result.push((start..s.len(), &s[start..s.len()])); - } - - result -} - -pub fn default() -> D { - Default::default() -} - -pub use self::shell::{ - get_default_system_shell, get_default_system_shell_preferring_bash, get_system_shell, -}; - -#[derive(Debug)] -pub enum ConnectionResult { - Timeout, - ConnectionReset, - Result(anyhow::Result), -} - -impl ConnectionResult { - pub fn into_response(self) -> anyhow::Result { - match self { - ConnectionResult::Timeout => anyhow::bail!("Request timed out"), - ConnectionResult::ConnectionReset => anyhow::bail!("Server reset the connection"), - ConnectionResult::Result(r) => r, - } - } -} - -impl From> for ConnectionResult { - fn from(result: anyhow::Result) -> Self { - ConnectionResult::Result(result) - } -} - -#[track_caller] -pub fn some_or_debug_panic(option: Option) -> Option { - #[cfg(debug_assertions)] - if option.is_none() { - panic!("Unexpected None"); - } - option -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extend_sorted() { - let mut vec = vec![]; - - extend_sorted(&mut vec, vec![21, 17, 13, 8, 1, 0], 5, |a, b| b.cmp(a)); - assert_eq!(vec, &[21, 17, 13, 8, 1]); - - extend_sorted(&mut vec, vec![101, 19, 17, 8, 2], 8, |a, b| b.cmp(a)); - assert_eq!(vec, &[101, 21, 19, 17, 13, 8, 2, 1]); - - extend_sorted(&mut vec, vec![1000, 19, 17, 9, 5], 8, |a, b| b.cmp(a)); - assert_eq!(vec, &[1000, 101, 21, 19, 17, 13, 9, 8]); - } - - #[test] - fn test_truncate_to_bottom_n_sorted_by() { - let mut vec: Vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 10, &u32::cmp); - assert_eq!(vec, &[1, 2, 3, 4, 5]); - - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 5, &u32::cmp); - assert_eq!(vec, &[1, 2, 3, 4, 5]); - - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 4, &u32::cmp); - assert_eq!(vec, &[1, 2, 3, 4]); - - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 1, &u32::cmp); - assert_eq!(vec, &[1]); - - vec = vec![5, 2, 3, 4, 1]; - truncate_to_bottom_n_sorted_by(&mut vec, 0, &u32::cmp); - assert!(vec.is_empty()); - } - - #[test] - fn test_iife() { - fn option_returning_function() -> Option<()> { - None - } - - let foo = maybe!({ - option_returning_function()?; - Some(()) - }); - - assert_eq!(foo, None); - } - - #[test] - fn test_truncate_and_trailoff() { - assert_eq!(truncate_and_trailoff("", 5), ""); - assert_eq!(truncate_and_trailoff("aaaaaa", 7), "aaaaaa"); - assert_eq!(truncate_and_trailoff("aaaaaa", 6), "aaaaaa"); - assert_eq!(truncate_and_trailoff("aaaaaa", 5), "aaaaa…"); - assert_eq!(truncate_and_trailoff("èèèèèè", 7), "èèèèèè"); - assert_eq!(truncate_and_trailoff("èèèèèè", 6), "èèèèèè"); - assert_eq!(truncate_and_trailoff("èèèèèè", 5), "èèèèè…"); - } - - #[test] - fn test_truncate_and_remove_front() { - assert_eq!(truncate_and_remove_front("", 5), ""); - assert_eq!(truncate_and_remove_front("aaaaaa", 7), "aaaaaa"); - assert_eq!(truncate_and_remove_front("aaaaaa", 6), "aaaaaa"); - assert_eq!(truncate_and_remove_front("aaaaaa", 5), "…aaaaa"); - assert_eq!(truncate_and_remove_front("èèèèèè", 7), "èèèèèè"); - assert_eq!(truncate_and_remove_front("èèèèèè", 6), "èèèèèè"); - assert_eq!(truncate_and_remove_front("èèèèèè", 5), "…èèèèè"); - } - - #[test] - fn test_numeric_prefix_str_method() { - let target = "1a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), "a") - ); - - let target = "12ab"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), "ab") - ); - - let target = "12_ab"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), "_ab") - ); - - let target = "1_2ab"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), "_2ab") - ); - - let target = "1.2"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), ".2") - ); - - let target = "1.2_a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(1), ".2_a") - ); - - let target = "12.2_a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), ".2_a") - ); - - let target = "12a.2_a"; - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(target), - NumericPrefixWithSuffix(Some(12), "a.2_a") - ); - } - - #[test] - fn test_numeric_prefix_with_suffix() { - let mut sorted = vec!["1-abc", "10", "11def", "2", "21-abc"]; - sorted.sort_by_key(|s| NumericPrefixWithSuffix::from_numeric_prefixed_str(s)); - assert_eq!(sorted, ["1-abc", "2", "10", "11def", "21-abc"]); - - for numeric_prefix_less in ["numeric_prefix_less", "aaa", "~™£"] { - assert_eq!( - NumericPrefixWithSuffix::from_numeric_prefixed_str(numeric_prefix_less), - NumericPrefixWithSuffix(None, numeric_prefix_less), - "String without numeric prefix `{numeric_prefix_less}` should not be converted into NumericPrefixWithSuffix" - ) - } - } - - #[test] - fn test_word_consists_of_emojis() { - let words_to_test = vec![ - ("👨‍👩‍👧‍👧👋🥒", true), - ("👋", true), - ("!👋", false), - ("👋!", false), - ("👋 ", false), - (" 👋", false), - ("Test", false), - ]; - - for (text, expected_result) in words_to_test { - assert_eq!(word_consists_of_emojis(text), expected_result); - } - } - - #[test] - fn test_truncate_lines_and_trailoff() { - let text = r#"Line 1 -Line 2 -Line 3"#; - - assert_eq!( - truncate_lines_and_trailoff(text, 2), - r#"Line 1 -…"# - ); - - assert_eq!( - truncate_lines_and_trailoff(text, 3), - r#"Line 1 -Line 2 -…"# - ); - - assert_eq!( - truncate_lines_and_trailoff(text, 4), - r#"Line 1 -Line 2 -Line 3"# - ); - } - - #[test] - fn test_expanded_and_wrapped_usize_range() { - // Neither wrap - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 1, 1, 8).collect::>(), - (1..5).collect::>() - ); - // Start wraps - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 3, 1, 8).collect::>(), - ((0..5).chain(7..8)).collect::>() - ); - // Start wraps all the way around - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 5, 1, 8).collect::>(), - (0..8).collect::>() - ); - // Start wraps all the way around and past 0 - assert_eq!( - expanded_and_wrapped_usize_range(2..4, 10, 1, 8).collect::>(), - (0..8).collect::>() - ); - // End wraps - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 1, 4, 8).collect::>(), - (0..1).chain(2..8).collect::>() - ); - // End wraps all the way around - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 1, 5, 8).collect::>(), - (0..8).collect::>() - ); - // End wraps all the way around and past the end - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 1, 10, 8).collect::>(), - (0..8).collect::>() - ); - // Both start and end wrap - assert_eq!( - expanded_and_wrapped_usize_range(3..5, 4, 4, 8).collect::>(), - (0..8).collect::>() - ); - } - - #[test] - fn test_wrapped_usize_outward_from() { - // No wrapping - assert_eq!( - wrapped_usize_outward_from(4, 2, 2, 10).collect::>(), - vec![4, 5, 3, 6, 2] - ); - // Wrapping at end - assert_eq!( - wrapped_usize_outward_from(8, 2, 3, 10).collect::>(), - vec![8, 9, 7, 0, 6, 1] - ); - // Wrapping at start - assert_eq!( - wrapped_usize_outward_from(1, 3, 2, 10).collect::>(), - vec![1, 2, 0, 3, 9, 8] - ); - // All values wrap around - assert_eq!( - wrapped_usize_outward_from(5, 10, 10, 8).collect::>(), - vec![5, 6, 4, 7, 3, 0, 2, 1] - ); - // None before / after - assert_eq!( - wrapped_usize_outward_from(3, 0, 0, 8).collect::>(), - vec![3] - ); - // Starting point already wrapped - assert_eq!( - wrapped_usize_outward_from(15, 2, 2, 10).collect::>(), - vec![5, 6, 4, 7, 3] - ); - // wrap_length of 0 - assert_eq!( - wrapped_usize_outward_from(4, 2, 2, 0).collect::>(), - Vec::::new() - ); - } - - #[test] - fn test_split_with_ranges() { - let input = "hi"; - let result = split_str_with_ranges(input, |c| c == ' '); - - assert_eq!(result.len(), 1); - assert_eq!(result[0], (0..2, "hi")); - - let input = "héllo🦀world"; - let result = split_str_with_ranges(input, |c| c == '🦀'); - - assert_eq!(result.len(), 2); - assert_eq!(result[0], (0..6, "héllo")); // 'é' is 2 bytes - assert_eq!(result[1], (10..15, "world")); // '🦀' is 4 bytes - } -} diff --git a/crates/util_macros/Cargo.toml b/crates/util_macros/Cargo.toml deleted file mode 100644 index f72955b3ae..0000000000 --- a/crates/util_macros/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "util_macros" -version = "0.1.0" -edition.workspace = true -publish = false -license = "Apache-2.0" -description = "Utility macros for Zed" - -[lints] -workspace = true - -[lib] -path = "src/util_macros.rs" -proc-macro = true -doctest = false - -[dependencies] -quote.workspace = true -syn.workspace = true -perf.workspace = true - -[features] -perf-enabled = [] diff --git a/crates/util_macros/LICENSE-APACHE b/crates/util_macros/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/util_macros/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/util_macros/src/util_macros.rs b/crates/util_macros/src/util_macros.rs deleted file mode 100644 index 4973e41de2..0000000000 --- a/crates/util_macros/src/util_macros.rs +++ /dev/null @@ -1,286 +0,0 @@ -#![cfg_attr(not(target_os = "windows"), allow(unused))] -#![allow(clippy::test_attr_in_doctest)] - -use perf::*; -use proc_macro::TokenStream; -use quote::{ToTokens, quote}; -use syn::{ItemFn, LitStr, parse_macro_input, parse_quote}; - -/// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces -/// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is -/// returned unmodified. -/// -/// # Example -/// ```rust -/// use util_macros::path; -/// -/// let path = path!("/Users/user/file.txt"); -/// #[cfg(target_os = "windows")] -/// assert_eq!(path, "C:\\Users\\user\\file.txt"); -/// #[cfg(not(target_os = "windows"))] -/// assert_eq!(path, "/Users/user/file.txt"); -/// ``` -#[proc_macro] -pub fn path(input: TokenStream) -> TokenStream { - let path = parse_macro_input!(input as LitStr); - let mut path = path.value(); - - #[cfg(target_os = "windows")] - { - path = path.replace("/", "\\"); - if path.starts_with("\\") { - path = format!("C:{}", path); - } - } - - TokenStream::from(quote! { - #path - }) -} - -/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows. -/// But if the target OS is not Windows, the URI is returned as is. -/// -/// # Example -/// ```rust -/// use util_macros::uri; -/// -/// let uri = uri!("file:///path/to/file"); -/// #[cfg(target_os = "windows")] -/// assert_eq!(uri, "file:///C:/path/to/file"); -/// #[cfg(not(target_os = "windows"))] -/// assert_eq!(uri, "file:///path/to/file"); -/// ``` -#[proc_macro] -pub fn uri(input: TokenStream) -> TokenStream { - let uri = parse_macro_input!(input as LitStr); - let uri = uri.value(); - - #[cfg(target_os = "windows")] - let uri = uri.replace("file:///", "file:///C:/"); - - TokenStream::from(quote! { - #uri - }) -} - -/// This macro replaces the line endings `\n` with `\r\n` for Windows. -/// But if the target OS is not Windows, the line endings are returned as is. -/// -/// # Example -/// ```rust -/// use util_macros::line_endings; -/// -/// let text = line_endings!("Hello\nWorld"); -/// #[cfg(target_os = "windows")] -/// assert_eq!(text, "Hello\r\nWorld"); -/// #[cfg(not(target_os = "windows"))] -/// assert_eq!(text, "Hello\nWorld"); -/// ``` -#[proc_macro] -pub fn line_endings(input: TokenStream) -> TokenStream { - let text = parse_macro_input!(input as LitStr); - let text = text.value(); - - #[cfg(target_os = "windows")] - let text = text.replace("\n", "\r\n"); - - TokenStream::from(quote! { - #text - }) -} - -/// Inner data for the perf macro. -#[derive(Default)] -struct PerfArgs { - /// How many times to loop a test before rerunning the test binary. If left - /// empty, the test harness will auto-determine this value. - iterations: Option, - /// How much this test's results should be weighed when comparing across runs. - /// If unspecified, defaults to `WEIGHT_DEFAULT` (50). - weight: Option, - /// How relevant a benchmark is to overall performance. See docs on the enum - /// for details. If unspecified, `Average` is selected. - importance: Importance, -} - -#[warn(clippy::all, clippy::pedantic)] -impl PerfArgs { - /// Parses attribute arguments into a `PerfArgs`. - fn parse_into(&mut self, meta: syn::meta::ParseNestedMeta) -> syn::Result<()> { - if meta.path.is_ident("iterations") { - self.iterations = Some(meta.value()?.parse()?); - } else if meta.path.is_ident("weight") { - self.weight = Some(meta.value()?.parse()?); - } else if meta.path.is_ident("critical") { - self.importance = Importance::Critical; - } else if meta.path.is_ident("important") { - self.importance = Importance::Important; - } else if meta.path.is_ident("average") { - // This shouldn't be specified manually, but oh well. - self.importance = Importance::Average; - } else if meta.path.is_ident("iffy") { - self.importance = Importance::Iffy; - } else if meta.path.is_ident("fluff") { - self.importance = Importance::Fluff; - } else { - return Err(syn::Error::new_spanned(meta.path, "unexpected identifier")); - } - Ok(()) - } -} - -/// Marks a test as perf-sensitive, to be triaged when checking the performance -/// of a build. This also automatically applies `#[test]`. -/// -/// # Usage -/// Applying this attribute to a test marks it as average importance by default. -/// There are 5 levels of importance (`Critical`, `Important`, `Average`, `Iffy`, -/// `Fluff`); see the documentation on `Importance` for details. Add the importance -/// as a parameter to override the default (e.g. `#[perf(important)]`). -/// -/// Each test also has a weight factor. This is irrelevant on its own, but is considered -/// when comparing results across different runs. By default, this is set to 50; -/// pass `weight = n` as a parameter to override this. Note that this value is only -/// relevant within its importance category. -/// -/// By default, the number of iterations when profiling this test is auto-determined. -/// If this needs to be overwritten, pass the desired iteration count as a parameter -/// (`#[perf(iterations = n)]`). Note that the actual profiler may still run the test -/// an arbitrary number times; this flag just sets the number of executions before the -/// process is restarted and global state is reset. -/// -/// This attribute should probably not be applied to tests that do any significant -/// disk IO, as locks on files may not be released in time when repeating a test many -/// times. This might lead to spurious failures. -/// -/// # Examples -/// ```rust -/// use util_macros::perf; -/// -/// #[perf] -/// fn generic_test() { -/// // Test goes here. -/// } -/// -/// #[perf(fluff, weight = 30)] -/// fn cold_path_test() { -/// // Test goes here. -/// } -/// ``` -/// -/// This also works with `#[gpui::test]`s, though in most cases it shouldn't -/// be used with automatic iterations. -/// ```rust,ignore -/// use util_macros::perf; -/// -/// #[perf(iterations = 1, critical)] -/// #[gpui::test] -/// fn oneshot_test(_cx: &mut gpui::TestAppContext) { -/// // Test goes here. -/// } -/// ``` -#[proc_macro_attribute] -#[warn(clippy::all, clippy::pedantic)] -pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream { - let mut args = PerfArgs::default(); - let parser = syn::meta::parser(|meta| PerfArgs::parse_into(&mut args, meta)); - parse_macro_input!(our_attr with parser); - - let ItemFn { - attrs: mut attrs_main, - vis, - sig: mut sig_main, - block, - } = parse_macro_input!(input as ItemFn); - if !attrs_main - .iter() - .any(|a| Some(&parse_quote!(test)) == a.path().segments.last()) - { - attrs_main.push(parse_quote!(#[test])); - } - attrs_main.push(parse_quote!(#[allow(non_snake_case)])); - - let fns = if cfg!(perf_enabled) { - #[allow(clippy::wildcard_imports, reason = "We control the other side")] - use consts::*; - - // Make the ident obvious when calling, for the test parser. - // Also set up values for the second metadata-returning "test". - let mut new_ident_main = sig_main.ident.to_string(); - let mut new_ident_meta = new_ident_main.clone(); - new_ident_main.push_str(SUF_NORMAL); - new_ident_meta.push_str(SUF_MDATA); - - let new_ident_main = syn::Ident::new(&new_ident_main, sig_main.ident.span()); - sig_main.ident = new_ident_main; - - // We don't want any nonsense if the original test had a weird signature. - let new_ident_meta = syn::Ident::new(&new_ident_meta, sig_main.ident.span()); - let sig_meta = parse_quote!(fn #new_ident_meta()); - let attrs_meta = parse_quote!(#[test] #[allow(non_snake_case)]); - - // Make the test loop as the harness instructs it to. - let block_main = { - // The perf harness will pass us the value in an env var. Even if we - // have a preset value, just do this to keep the code paths unified. - parse_quote!({ - let iter_count = std::env::var(#ITER_ENV_VAR).unwrap().parse::().unwrap(); - for _ in 0..iter_count { - #block - } - }) - }; - let importance = format!("{}", args.importance); - let block_meta = { - // This function's job is to just print some relevant info to stdout, - // based on the params this attr is passed. It's not an actual test. - // Since we use a custom attr set on our metadata fn, it shouldn't - // cause problems with xfail tests. - let q_iter = if let Some(iter) = args.iterations { - quote! { - println!("{} {} {}", #MDATA_LINE_PREF, #ITER_COUNT_LINE_NAME, #iter); - } - } else { - quote! {} - }; - let weight = args - .weight - .unwrap_or_else(|| parse_quote! { #WEIGHT_DEFAULT }); - parse_quote!({ - #q_iter - println!("{} {} {}", #MDATA_LINE_PREF, #WEIGHT_LINE_NAME, #weight); - println!("{} {} {}", #MDATA_LINE_PREF, #IMPORTANCE_LINE_NAME, #importance); - println!("{} {} {}", #MDATA_LINE_PREF, #VERSION_LINE_NAME, #MDATA_VER); - }) - }; - - vec![ - // The real test. - ItemFn { - attrs: attrs_main, - vis: vis.clone(), - sig: sig_main, - block: block_main, - }, - // The fake test. - ItemFn { - attrs: attrs_meta, - vis, - sig: sig_meta, - block: block_meta, - }, - ] - } else { - vec![ItemFn { - attrs: attrs_main, - vis, - sig: sig_main, - block, - }] - }; - - fns.into_iter() - .flat_map(|f| TokenStream::from(f.into_token_stream())) - .collect() -} diff --git a/crates/watch/Cargo.toml b/crates/watch/Cargo.toml deleted file mode 100644 index 8f61d1f031..0000000000 --- a/crates/watch/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "watch" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/watch.rs" -doctest = true - -[dependencies] -parking_lot.workspace = true - -[dev-dependencies] -ctor.workspace = true -env_logger.workspace = true -futures.workspace = true -gpui = { workspace = true, features = ["test-support"] } -log.workspace = true -rand.workspace = true diff --git a/crates/watch/LICENSE-APACHE b/crates/watch/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/watch/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/watch/src/error.rs b/crates/watch/src/error.rs deleted file mode 100644 index 231676cb73..0000000000 --- a/crates/watch/src/error.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Watch error types. - -use std::fmt; - -#[derive(Debug, Eq, PartialEq)] -pub struct NoReceiverError; - -impl fmt::Display for NoReceiverError { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(fmt, "all receivers were dropped") - } -} - -impl std::error::Error for NoReceiverError {} - -#[derive(Debug, Eq, PartialEq)] -pub struct NoSenderError; - -impl fmt::Display for NoSenderError { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(fmt, "sender was dropped") - } -} - -impl std::error::Error for NoSenderError {} diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs deleted file mode 100644 index aee602db86..0000000000 --- a/crates/watch/src/watch.rs +++ /dev/null @@ -1,292 +0,0 @@ -mod error; - -pub use error::*; -use parking_lot::{RwLock, RwLockReadGuard, RwLockUpgradableReadGuard}; -use std::{ - collections::BTreeMap, - mem, - pin::Pin, - sync::Arc, - task::{Context, Poll, Waker}, -}; - -pub fn channel(value: T) -> (Sender, Receiver) { - let state = Arc::new(RwLock::new(State { - value, - wakers: BTreeMap::new(), - next_waker_id: WakerId::default(), - version: 0, - closed: false, - })); - - ( - Sender { - state: state.clone(), - }, - Receiver { state, version: 0 }, - ) -} - -#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct WakerId(usize); - -impl WakerId { - fn post_inc(&mut self) -> Self { - let id = *self; - self.0 = id.0.wrapping_add(1); - *self - } -} - -struct State { - value: T, - wakers: BTreeMap, - next_waker_id: WakerId, - version: usize, - closed: bool, -} - -pub struct Sender { - state: Arc>>, -} - -impl Sender { - pub fn receiver(&self) -> Receiver { - let version = self.state.read().version; - Receiver { - state: self.state.clone(), - version, - } - } - - pub fn send(&mut self, value: T) -> Result<(), NoReceiverError> { - if let Some(state) = Arc::get_mut(&mut self.state) { - let state = state.get_mut(); - state.value = value; - debug_assert_eq!(state.wakers.len(), 0); - Err(NoReceiverError) - } else { - let mut state = self.state.write(); - state.value = value; - state.version = state.version.wrapping_add(1); - let wakers = mem::take(&mut state.wakers); - drop(state); - - for (_, waker) in wakers { - waker.wake(); - } - - Ok(()) - } - } -} - -impl Drop for Sender { - fn drop(&mut self) { - let mut state = self.state.write(); - state.closed = true; - for (_, waker) in mem::take(&mut state.wakers) { - waker.wake(); - } - } -} - -#[derive(Clone)] -pub struct Receiver { - state: Arc>>, - version: usize, -} - -struct Changed<'a, T> { - receiver: &'a mut Receiver, - pending_waker_id: Option, -} - -impl Future for Changed<'_, T> { - type Output = Result<(), NoSenderError>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { - let this = &mut *self; - - let state = this.receiver.state.upgradable_read(); - if state.version != this.receiver.version { - // The sender produced a new value. Avoid unregistering the pending - // waker, because the sender has already done so. - this.pending_waker_id = None; - this.receiver.version = state.version; - Poll::Ready(Ok(())) - } else if state.closed { - Poll::Ready(Err(NoSenderError)) - } else { - let mut state = RwLockUpgradableReadGuard::upgrade(state); - - // Unregister the pending waker. This should happen automatically - // when the waker gets awoken by the sender, but if this future was - // polled again without an explicit call to `wake` (e.g., a spurious - // wake by the executor), we need to remove it manually. - if let Some(pending_waker_id) = this.pending_waker_id.take() { - state.wakers.remove(&pending_waker_id); - } - - // Register the waker for this future. - let waker_id = state.next_waker_id.post_inc(); - state.wakers.insert(waker_id, cx.waker().clone()); - this.pending_waker_id = Some(waker_id); - - Poll::Pending - } - } -} - -impl Drop for Changed<'_, T> { - fn drop(&mut self) { - // If this future gets dropped before the waker has a chance of being - // awoken, we need to clear it to avoid a memory leak. - if let Some(waker_id) = self.pending_waker_id { - let mut state = self.receiver.state.write(); - state.wakers.remove(&waker_id); - } - } -} - -impl Receiver { - pub fn borrow(&mut self) -> parking_lot::MappedRwLockReadGuard<'_, T> { - let state = self.state.read(); - self.version = state.version; - RwLockReadGuard::map(state, |state| &state.value) - } - - pub fn changed(&mut self) -> impl Future> { - Changed { - receiver: self, - pending_waker_id: None, - } - } - - /// Creates a new [`Receiver`] holding an initial value that will never change. - pub fn constant(value: T) -> Self { - let state = Arc::new(RwLock::new(State { - value, - wakers: BTreeMap::new(), - next_waker_id: WakerId::default(), - version: 0, - closed: false, - })); - - Self { state, version: 0 } - } -} - -impl Receiver { - pub async fn recv(&mut self) -> Result { - self.changed().await?; - Ok(self.borrow().clone()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::{FutureExt, select_biased}; - use gpui::{AppContext, TestAppContext}; - use std::{ - pin::pin, - sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - }; - - #[gpui::test] - async fn test_basic_watch() { - let (mut sender, mut receiver) = channel(0); - assert_eq!(sender.send(1), Ok(())); - assert_eq!(receiver.recv().await, Ok(1)); - - assert_eq!(sender.send(2), Ok(())); - assert_eq!(sender.send(3), Ok(())); - assert_eq!(receiver.recv().await, Ok(3)); - - drop(receiver); - assert_eq!(sender.send(4), Err(NoReceiverError)); - - let mut receiver = sender.receiver(); - assert_eq!(sender.send(5), Ok(())); - assert_eq!(receiver.recv().await, Ok(5)); - - // Ensure `changed` doesn't resolve if we just read the latest value - // using `borrow`. - assert_eq!(sender.send(6), Ok(())); - assert_eq!(*receiver.borrow(), 6); - assert_eq!(receiver.changed().now_or_never(), None); - - assert_eq!(sender.send(7), Ok(())); - drop(sender); - assert_eq!(receiver.recv().await, Ok(7)); - assert_eq!(receiver.recv().await, Err(NoSenderError)); - } - - #[gpui::test(iterations = 1000)] - async fn test_watch_random(cx: &mut TestAppContext) { - let next_id = Arc::new(AtomicUsize::new(1)); - let closed = Arc::new(AtomicBool::new(false)); - let (mut tx, rx) = channel(0); - let mut tasks = Vec::new(); - - tasks.push(cx.background_spawn({ - let executor = cx.executor(); - let next_id = next_id.clone(); - let closed = closed.clone(); - async move { - for _ in 0..16 { - executor.simulate_random_delay().await; - let id = next_id.fetch_add(1, SeqCst); - log::info!("sending {}", id); - tx.send(id).ok(); - } - closed.store(true, SeqCst); - } - })); - - for receiver_id in 0..16 { - let executor = cx.executor().clone(); - let next_id = next_id.clone(); - let closed = closed.clone(); - let mut rx = rx.clone(); - let mut prev_observed_value = *rx.borrow(); - tasks.push(cx.background_spawn(async move { - for _ in 0..16 { - executor.simulate_random_delay().await; - - log::info!("{}: receiving", receiver_id); - let mut timeout = executor.simulate_random_delay().fuse(); - let mut recv = pin!(rx.recv().fuse()); - select_biased! { - _ = timeout => { - log::info!("{}: dropping recv future", receiver_id); - } - result = recv => { - match result { - Ok(value) => { - log::info!("{}: received {}", receiver_id, value); - assert_eq!(value, next_id.load(SeqCst) - 1); - assert_ne!(value, prev_observed_value); - prev_observed_value = value; - } - Err(NoSenderError) => { - log::info!("{}: closed", receiver_id); - assert!(closed.load(SeqCst)); - break; - } - } - } - } - } - })); - } - - futures::future::join_all(tasks).await; - } - - #[ctor::ctor] - fn init_logger() { - let _ = env_logger::try_init(); - } -} diff --git a/crates/gpui/docs/contexts.md b/docs/contexts.md similarity index 100% rename from crates/gpui/docs/contexts.md rename to docs/contexts.md diff --git a/crates/gpui/docs/key_dispatch.md b/docs/key_dispatch.md similarity index 100% rename from crates/gpui/docs/key_dispatch.md rename to docs/key_dispatch.md diff --git a/crates/gpui/examples/data_table.rs b/examples/bench/data_table.rs similarity index 100% rename from crates/gpui/examples/data_table.rs rename to examples/bench/data_table.rs diff --git a/crates/gpui/examples/paths_bench.rs b/examples/bench/paths_bench.rs similarity index 100% rename from crates/gpui/examples/paths_bench.rs rename to examples/bench/paths_bench.rs diff --git a/crates/gpui/examples/pattern.rs b/examples/bench/pattern.rs similarity index 100% rename from crates/gpui/examples/pattern.rs rename to examples/bench/pattern.rs diff --git a/crates/gpui/examples/shadow.rs b/examples/bench/shadow.rs similarity index 100% rename from crates/gpui/examples/shadow.rs rename to examples/bench/shadow.rs diff --git a/examples/learn/animation.rs b/examples/learn/animation.rs new file mode 100644 index 0000000000..6d1e5a17b2 --- /dev/null +++ b/examples/learn/animation.rs @@ -0,0 +1,278 @@ +//! Animation Example +//! +//! This example demonstrates animation capabilities in GPUI: +//! +//! 1. Basic animations with `with_animation` +//! 2. Easing functions - ease_in_out, bounce, linear +//! 3. Transformations - rotate, scale, translate +//! 4. Repeating and duration controls + +#[path = "../prelude.rs"] +mod example_prelude; + +use std::time::Duration; + +use anyhow::Result; +use gpui::{ + Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Colors, Context, Hsla, + SharedString, Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, + linear, percentage, prelude::*, px, size as gpui_size, svg, +}; + +struct Assets {} + +impl AssetSource for Assets { + fn load(&self, path: &str) -> Result>> { + std::fs::read(path) + .map(Into::into) + .map_err(Into::into) + .map(Some) + } + + fn list(&self, path: &str) -> Result> { + Ok(std::fs::read_dir(path)? + .filter_map(|entry| { + Some(SharedString::from( + entry.ok()?.path().to_string_lossy().into_owned(), + )) + }) + .collect::>()) + } +} + +const ARROW_CIRCLE_SVG: &str = concat!( + env!("CARGO_MANIFEST_DIR"), + "/examples/legacy/image/arrow_circle.svg" +); + +struct AnimationExample; + +impl Render for AnimationExample { + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(600.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Animation Patterns"), + ) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Animations, easing, and transformations in GPUI"), + ), + ) + .child(section( + &colors, + "Rotation Animation", + rotation_example(&colors), + )) + .child(section(&colors, "Bounce Easing", bounce_example(&colors))) + .child(section(&colors, "Scale Animation", scale_example(&colors))) + .child(section( + &colors, + "Combined Animations", + combined_example(&colors), + )), + ) + } +} + +fn rotation_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let accent = colors.accent; + + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Continuous rotation with ease_in_out easing"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(accent) + .with_animation( + "rotation", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(ease_in_out), + |svg, delta| { + svg.with_transformation(Transformation::rotate(percentage(delta))) + }, + ), + ), + ) +} + +fn bounce_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let success = colors.success; + + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Bouncing rotation with bounce(ease_in_out)"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(success) + .with_animation( + "bounce_rotation", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(bounce(ease_in_out)), + |svg, delta| { + svg.with_transformation(Transformation::rotate(percentage(delta))) + }, + ), + ), + ) +} + +fn scale_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let warning = colors.warning; + + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Scale pulsing with linear easing"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(warning) + .with_animation( + "scale", + Animation::new(Duration::from_millis(1500)) + .repeat() + .with_easing(bounce(linear)), + |svg, delta| { + let scale = 0.8 + (delta * 0.4); + svg.with_transformation(Transformation::scale(gpui_size(scale, scale))) + }, + ), + ), + ) +} + +fn combined_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let error = colors.error; + + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Rotation + scale combined"), + ) + .child( + div().flex().items_center().justify_center().h_24().child( + svg() + .size_16() + .overflow_hidden() + .path(ARROW_CIRCLE_SVG) + .text_color(error) + .with_animation( + "combined", + Animation::new(Duration::from_secs(3)) + .repeat() + .with_easing(ease_in_out), + |svg, delta| { + let scale = 0.7 + (delta * 0.6); + svg.with_transformation( + Transformation::rotate(percentage(delta)) + .with_scaling(gpui_size(scale, scale)), + ) + }, + ), + ), + ) +} + +fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { + let surface: Hsla = colors.surface.into(); + + div() + .flex() + .flex_col() + .gap_2() + .p_4() + .bg(surface.opacity(0.5)) + .rounded_lg() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) +} + +fn main() { + Application::new() + .with_assets(Assets {}) + .run(|cx: &mut App| { + let bounds = Bounds::centered(None, gpui_size(px(500.), px(650.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| AnimationExample), + ) + .expect("Failed to open window"); + + example_prelude::init_example(cx, "Animation"); + }); +} diff --git a/examples/learn/async_tasks.rs b/examples/learn/async_tasks.rs new file mode 100644 index 0000000000..928b8c93c7 --- /dev/null +++ b/examples/learn/async_tasks.rs @@ -0,0 +1,561 @@ +//! Async Tasks Example +//! +//! This example demonstrates different async patterns in GPUI: +//! +//! 1. `cx.spawn` - Foreground tasks for UI updates +//! 2. `cx.background_spawn` - Background tasks for heavy computation +//! 3. Task management - Storing, canceling, and detaching tasks +//! 4. Progress updates - Communicating from background to UI + +#[path = "../prelude.rs"] +mod example_prelude; + +use std::time::Duration; + +use gpui::{ + App, Application, Bounds, Colors, Context, Entity, Render, Task, Window, WindowBounds, + WindowOptions, div, prelude::*, px, size, +}; + +// Example 1: Simple Foreground Task +// +// `cx.spawn` runs an async closure on the foreground thread. +// Use it when you need to perform async work that updates UI. + +struct ForegroundTaskDemo { + message: String, + is_loading: bool, +} + +impl ForegroundTaskDemo { + fn new() -> Self { + Self { + message: "Click to start a foreground task".into(), + is_loading: false, + } + } + + fn start_task(&mut self, cx: &mut Context) { + self.is_loading = true; + self.message = "Loading...".into(); + cx.notify(); + + cx.spawn(async move |this, cx| { + cx.background_spawn(async { + std::thread::sleep(Duration::from_secs(1)); + }) + .await; + + this.update(cx, |this, cx| { + this.message = "Task completed!".into(); + this.is_loading = false; + cx.notify(); + }) + .ok(); + }) + .detach(); + } +} + +// Example 2: Background Task with Progress +// +// `cx.background_spawn` runs work off the UI thread. +// Use it for heavy computation that shouldn't block the UI. + +struct BackgroundTaskDemo { + progress: u32, + result: Option, + is_computing: bool, +} + +impl BackgroundTaskDemo { + fn new() -> Self { + Self { + progress: 0, + result: None, + is_computing: false, + } + } + + fn start_computation(&mut self, cx: &mut Context) { + self.is_computing = true; + self.progress = 0; + self.result = None; + cx.notify(); + + cx.spawn(async move |this, cx| { + for i in 0..100 { + let computation = cx.background_spawn(async move { + std::thread::sleep(Duration::from_millis(10)); + (i + 1) as u64 + }); + + let partial_result = computation.await; + + this.update(cx, |this, cx| { + this.progress = i as u32 + 1; + this.result = Some(partial_result); + cx.notify(); + }) + .ok(); + } + + this.update(cx, |this, cx| { + this.is_computing = false; + cx.notify(); + }) + .ok(); + }) + .detach(); + } +} + +// Example 3: Cancellable Task +// +// Tasks can be cancelled by dropping them. +// Store a task in a field to keep it running. + +struct CancellableTaskDemo { + counter: u32, + counting_task: Option>, +} + +impl CancellableTaskDemo { + fn new() -> Self { + Self { + counter: 0, + counting_task: None, + } + } + + fn is_running(&self) -> bool { + self.counting_task.is_some() + } + + fn toggle(&mut self, cx: &mut Context) { + if self.counting_task.is_some() { + self.counting_task = None; + cx.notify(); + } else { + self.counting_task = Some(cx.spawn(async move |this, cx| { + loop { + cx.background_spawn(async { + std::thread::sleep(Duration::from_millis(100)); + }) + .await; + + let should_continue = this + .update(cx, |this, cx| { + this.counter += 1; + cx.notify(); + true + }) + .unwrap_or(false); + + if !should_continue { + break; + } + } + })); + cx.notify(); + } + } +} + +// Example 4: Task with Return Value +// +// Tasks can return values that you can await. + +struct ReturnValueDemo { + numbers: Vec, + sum: Option, + is_calculating: bool, +} + +impl ReturnValueDemo { + fn new() -> Self { + Self { + numbers: vec![1, 2, 3, 4, 5], + sum: None, + is_calculating: false, + } + } + + fn calculate_sum(&mut self, cx: &mut Context) { + self.is_calculating = true; + cx.notify(); + + let numbers = self.numbers.clone(); + + cx.spawn(async move |this, cx| { + let result = cx + .background_spawn(async move { + std::thread::sleep(Duration::from_millis(500)); + numbers.iter().sum::() + }) + .await; + + this.update(cx, |this, cx| { + this.sum = Some(result); + this.is_calculating = false; + cx.notify(); + }) + .ok(); + }) + .detach(); + } + + fn randomize(&mut self, cx: &mut Context) { + use std::collections::hash_map::RandomState; + use std::hash::{BuildHasher, Hasher}; + let hasher = RandomState::new().build_hasher().finish(); + self.numbers = (0..5) + .map(|i| ((hasher >> (i * 8)) & 0xFF) as i32 % 100) + .collect(); + self.sum = None; + cx.notify(); + } +} + +// Main Application + +struct AsyncTasksExample { + foreground_demo: Entity, + background_demo: Entity, + cancellable_demo: Entity, + return_demo: Entity, +} + +impl AsyncTasksExample { + fn new(cx: &mut Context) -> Self { + Self { + foreground_demo: cx.new(|_| ForegroundTaskDemo::new()), + background_demo: cx.new(|_| BackgroundTaskDemo::new()), + cancellable_demo: cx.new(|_| CancellableTaskDemo::new()), + return_demo: cx.new(|_| ReturnValueDemo::new()), + } + } +} + +impl Render for AsyncTasksExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let foreground = self.foreground_demo.read(cx); + let background = self.background_demo.read(cx); + let cancellable = self.cancellable_demo.read(cx); + let return_demo = self.return_demo.read(cx); + + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(500.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Async Tasks"), + ) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Spawning, background work, and task management"), + ), + ) + .child(demo_section( + &colors, + "1. Foreground Task (cx.spawn)", + "Runs async work on the UI thread. Good for sequential async operations.", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .text_color(colors.text) + .child(foreground.message.clone()), + ) + .child( + button(&colors, "foreground-btn", "Start Task", foreground.is_loading) + .on_click(cx.listener(|this, _, _, cx| { + this.foreground_demo.update(cx, |demo, cx| { + demo.start_task(cx); + }); + })), + ), + )) + .child(demo_section( + &colors, + "2. Background Task (cx.background_spawn)", + "Runs heavy computation off the UI thread with progress updates.", + div() + .flex() + .flex_col() + .gap_2() + .child(progress_bar(&colors, background.progress)) + .child( + div() + .text_sm() + .text_color(colors.text) + .child(format!( + "Progress: {}% | Result: {}", + background.progress, + background + .result + .map(|r| r.to_string()) + .unwrap_or_else(|| "-".into()) + )), + ) + .child( + button(&colors, "background-btn", "Compute", background.is_computing) + .on_click(cx.listener(|this, _, _, cx| { + this.background_demo.update(cx, |demo, cx| { + demo.start_computation(cx); + }); + })), + ), + )) + .child(demo_section( + &colors, + "3. Cancellable Task", + "Store Task in a field to keep it running. Drop to cancel.", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child(format!("{}", cancellable.counter)), + ) + .child({ + let is_running = cancellable.is_running(); + let (bg, bg_hover) = if is_running { + (colors.error, colors.error_hover) + } else { + (colors.success, colors.success_hover) + }; + div() + .id("cancel-btn") + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(colors.text) + .cursor_pointer() + .bg(bg) + .hover(move |style| style.bg(bg_hover)) + .child(if is_running { "Stop" } else { "Start Counter" }) + .on_click(cx.listener(|this, _, _, cx| { + this.cancellable_demo.update(cx, |demo, cx| { + demo.toggle(cx); + }); + })) + }), + )) + .child(demo_section( + &colors, + "4. Task with Return Value", + "Tasks can return values that can be awaited or used in chained operations.", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child(format!("Numbers: {:?}", return_demo.numbers)), + ) + .child( + div() + .text_sm() + .text_color(colors.text) + .child(format!( + "Sum: {}", + if return_demo.is_calculating { + "Calculating...".into() + } else { + return_demo + .sum + .map(|s| s.to_string()) + .unwrap_or_else(|| "Not calculated".into()) + } + )), + ) + .child( + div() + .flex() + .gap_2() + .child( + button( + &colors, + "sum-btn", + "Calculate Sum", + return_demo.is_calculating, + ) + .on_click(cx.listener(|this, _, _, cx| { + this.return_demo.update(cx, |demo, cx| { + demo.calculate_sum(cx); + }); + })), + ) + .child( + secondary_button(&colors, "random-btn", "Randomize") + .on_click(cx.listener(|this, _, _, cx| { + this.return_demo.update(cx, |demo, cx| { + demo.randomize(cx); + }); + })), + ), + ), + )) + + ) + } +} + +// Helper Components + +fn demo_section( + colors: &Colors, + title: &'static str, + description: &'static str, + content: impl IntoElement, +) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.surface) + .border_1() + .border_color(colors.border) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child(description), + ), + ) + .child(content) +} + +fn button( + colors: &Colors, + id: impl Into, + label: &'static str, + disabled: bool, +) -> gpui::Stateful { + let disabled_bg = colors.surface_hover; + let bg = colors.accent; + let bg_hover = colors.accent_hover; + let bg_active = colors.accent_active; + let text = colors.selected_text; + + div() + .id(id) + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(text) + .when(disabled, |el| { + el.bg(disabled_bg).cursor_not_allowed().opacity(0.6) + }) + .when(!disabled, |el| { + el.bg(bg) + .cursor_pointer() + .hover(move |style| style.bg(bg_hover)) + .active(move |style| style.bg(bg_active)) + }) + .child(label) +} + +fn secondary_button( + colors: &Colors, + id: impl Into, + label: &'static str, +) -> gpui::Stateful { + let bg = colors.surface_hover; + let bg_hover = colors.border; + let text = colors.text; + + div() + .id(id) + .px_3() + .py_1p5() + .rounded_md() + .text_sm() + .text_color(text) + .bg(bg) + .cursor_pointer() + .hover(move |style| style.bg(bg_hover)) + .child(label) +} + +fn progress_bar(colors: &Colors, progress: u32) -> impl IntoElement { + let clamped = progress.min(100); + let bar_bg = colors.surface_hover; + let bar_fill = colors.success; + + div() + .h_2() + .w_full() + .rounded_full() + .bg(bar_bg) + .overflow_hidden() + .child( + div() + .h_full() + .rounded_full() + .bg(bar_fill) + .w(gpui::relative(clamped as f32 / 100.0)), + ) +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(550.), px(850.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| AsyncTasksExample::new(cx)), + ) + .expect("Failed to open window"); + + example_prelude::init_example(cx, "Async Tasks"); + }); +} diff --git a/examples/learn/creating_components.rs b/examples/learn/creating_components.rs new file mode 100644 index 0000000000..a8204869dc --- /dev/null +++ b/examples/learn/creating_components.rs @@ -0,0 +1,434 @@ +//! Creating Components Example +//! +//! This example demonstrates three different approaches to creating interactive +//! stateful components in GPUI: +//! +//! 1. `use_state` - Hook-like state scoped to an element's lifetime +//! 2. `RenderOnce` - Stateless component that receives state from parent +//! 3. `Render` - Entity-backed view with persistent internal state + +#[path = "../prelude.rs"] +mod example_prelude; + +use example_prelude::init_example; +use gpui::{ + App, Application, Bounds, Colors, Context, Entity, IntoElement, Render, RenderOnce, Window, + WindowBounds, WindowOptions, div, prelude::*, px, size, +}; + +// ============================================================================ +// Approach 1: use_state +// ============================================================================ +// +// `use_state` creates element-scoped state that persists across renders. +// It's similar to React's useState hook. The state is automatically tied +// to the element's identity via caller location or a provided key. +// +// Pros: +// - Simple, hook-like API +// - State is scoped to element lifetime +// - No boilerplate for simple state +// +// Cons: +// - Less explicit than Entity-backed state +// - State is tied to call site location + +struct UseStateCounter { + count: i32, +} + +fn use_state_counter(colors: &Colors, window: &mut Window, cx: &mut App) -> impl IntoElement { + let state: Entity = + window.use_state(cx, |_window, _cx| UseStateCounter { count: 0 }); + + let count = state.read(cx).count; + + let error = colors.error; + let error_hover = colors.error_hover; + let success = colors.success; + let success_hover = colors.success_hover; + + div() + .id("use-state-counter") + .flex() + .flex_col() + .gap_2() + .p_4() + .rounded_lg() + .bg(colors.surface) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("use_state Counter"), + ) + .child( + div() + .text_2xl() + .text_color(colors.text) + .child(format!("{}", count)), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("use-state-decrement") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(error_hover)) + .child("−") + .on_click({ + let state = state.clone(); + move |_, _, cx| { + state.update(cx, |state, cx| { + state.count -= 1; + cx.notify(); + }); + } + }), + ) + .child( + div() + .id("use-state-increment") + .px_3() + .py_1() + .rounded_md() + .bg(success) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(success_hover)) + .child("+") + .on_click(move |_, _, cx| { + state.update(cx, |state, cx| { + state.count += 1; + cx.notify(); + }); + }), + ), + ) +} + +// ============================================================================ +// Approach 2: RenderOnce +// ============================================================================ +// +// `RenderOnce` components are stateless and consumed when rendered. +// They receive all data as props and delegate state management to the parent. +// This is the recommended approach for presentational components. +// +// Pros: +// - Clear data flow (props down, events up) +// - Lightweight (no Entity allocation) +// - Easy to test +// - Highly composable +// +// Cons: +// - Cannot maintain internal state +// - Parent must manage all state + +#[derive(IntoElement)] +struct RenderOnceCounter { + colors: Colors, + count: i32, + on_increment: Option>, + on_decrement: Option>, +} + +impl RenderOnceCounter { + fn new(colors: Colors, count: i32) -> Self { + Self { + colors, + count, + on_increment: None, + on_decrement: None, + } + } + + fn on_increment(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self { + self.on_increment = Some(Box::new(callback)); + self + } + + fn on_decrement(mut self, callback: impl Fn(&mut Window, &mut App) + 'static) -> Self { + self.on_decrement = Some(Box::new(callback)); + self + } +} + +impl RenderOnce for RenderOnceCounter { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let colors = self.colors; + let error = colors.error; + let error_hover = colors.error_hover; + let success = colors.success; + let success_hover = colors.success_hover; + + div() + .id("render-once-counter") + .flex() + .flex_col() + .gap_2() + .p_4() + .rounded_lg() + .bg(colors.surface) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("RenderOnce Counter"), + ) + .child( + div() + .text_2xl() + .text_color(colors.text) + .child(format!("{}", self.count)), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("render-once-decrement") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(error_hover)) + .child("−") + .when_some(self.on_decrement, |element, callback| { + element.on_click(move |_, window, cx| callback(window, cx)) + }), + ) + .child( + div() + .id("render-once-increment") + .px_3() + .py_1() + .rounded_md() + .bg(success) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(success_hover)) + .child("+") + .when_some(self.on_increment, |element, callback| { + element.on_click(move |_, window, cx| callback(window, cx)) + }), + ), + ) + } +} + +// ============================================================================ +// Approach 3: Render (Entity-backed) +// ============================================================================ +// +// `Render` components are backed by an `Entity` and maintain their own +// internal state. This is the recommended approach for complex components +// that need to manage their own state, subscribe to events, or spawn tasks. +// +// Pros: +// - Full control over internal state +// - Can subscribe to events and observe other entities +// - Can spawn async tasks +// - Has identity (can be passed around as Entity) +// +// Cons: +// - More boilerplate +// - Higher memory overhead +// - More complex lifecycle + +struct RenderCounter { + count: i32, +} + +impl RenderCounter { + fn new() -> Self { + Self { count: 0 } + } + + fn increment(&mut self, _window: &mut Window, cx: &mut Context) { + self.count += 1; + cx.notify(); + } + + fn decrement(&mut self, _window: &mut Window, cx: &mut Context) { + self.count -= 1; + cx.notify(); + } +} + +impl Render for RenderCounter { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let error = colors.error; + let error_hover = colors.error_hover; + let success = colors.success; + let success_hover = colors.success_hover; + + div() + .id("render-counter") + .flex() + .flex_col() + .gap_2() + .p_4() + .rounded_lg() + .bg(colors.surface) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Render Counter"), + ) + .child( + div() + .text_2xl() + .text_color(colors.text) + .child(format!("{}", self.count)), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("render-decrement") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(error_hover)) + .child("−") + .on_click(cx.listener(|this, _, window, cx| { + this.decrement(window, cx); + })), + ) + .child( + div() + .id("render-increment") + .px_3() + .py_1() + .rounded_md() + .bg(success) + .text_color(colors.selected_text) + .cursor_pointer() + .hover(move |style| style.bg(success_hover)) + .child("+") + .on_click(cx.listener(|this, _, window, cx| { + this.increment(window, cx); + })), + ), + ) + } +} + +// ============================================================================ +// Main Application +// ============================================================================ + +struct CreatingComponentsExample { + render_counter: Entity, + render_once_count: i32, +} + +impl CreatingComponentsExample { + fn new(cx: &mut Context) -> Self { + Self { + render_counter: cx.new(|_| RenderCounter::new()), + render_once_count: 0, + } + } +} + +impl Render for CreatingComponentsExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let render_once_count = self.render_once_count; + let handle = cx.entity().downgrade(); + + div() + .id("main") + .size_full() + .flex() + .flex_col() + .gap_6() + .p_8() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_2xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Creating Components"), + ) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Three approaches to stateful components in GPUI"), + ), + ) + .child( + div() + .flex() + .flex_row() + .gap_4() + .child(use_state_counter(&colors, window, cx)) + .child( + RenderOnceCounter::new(colors.clone(), render_once_count) + .on_increment({ + let handle = handle.clone(); + move |_window, cx| { + handle + .update(cx, |this, cx| { + this.render_once_count += 1; + cx.notify(); + }) + .ok(); + } + }) + .on_decrement(move |_window, cx| { + handle + .update(cx, |this, cx| { + this.render_once_count -= 1; + cx.notify(); + }) + .ok(); + }), + ) + .child(self.render_counter.clone()), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(700.), px(400.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| CreatingComponentsExample::new(cx)), + ) + .expect("Failed to open window"); + + init_example(cx, "Creating Components"); + }); +} diff --git a/examples/learn/custom_drawing.rs b/examples/learn/custom_drawing.rs new file mode 100644 index 0000000000..ed7db83d55 --- /dev/null +++ b/examples/learn/custom_drawing.rs @@ -0,0 +1,479 @@ +//! Custom Drawing Example +//! +//! This example demonstrates custom drawing in GPUI using: +//! +//! 1. `canvas` element - For direct painting control +//! 2. `PathBuilder` - Creating custom vector shapes +//! 3. `window.paint_*` methods - Drawing quads, paths, and more +//! 4. Interactive drawing - Responding to mouse events + +use gpui::{ + App, Application, Bounds, Colors, Context, Hsla, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, Path, PathBuilder, Pixels, Point, Render, Rgba, Window, WindowBounds, + WindowOptions, canvas, div, fill, point, prelude::*, px, rgb, size, +}; + +#[path = "../prelude.rs"] +mod example_prelude; + +// Example 1: Basic Canvas Drawing +// +// The `canvas` element provides two callbacks: +// - prepaint: Called during layout to prepare drawing state +// - paint: Called during paint to actually draw + +fn basic_shapes_canvas(colors: &Colors) -> impl IntoElement { + let error = colors.error; + let success = colors.success; + let accent = colors.accent; + + canvas( + move |_bounds, _window, _cx| {}, + move |bounds, _prepaint_state, window, _cx| { + // Draw a filled rectangle + window.paint_quad(fill( + Bounds { + origin: point(bounds.origin.x + px(10.), bounds.origin.y + px(10.)), + size: size(px(60.), px(40.)), + }, + error, + )); + + // Draw another rectangle + window.paint_quad(fill( + Bounds { + origin: point(bounds.origin.x + px(80.), bounds.origin.y + px(10.)), + size: size(px(60.), px(40.)), + }, + success, + )); + + // Draw a third rectangle + window.paint_quad(fill( + Bounds { + origin: point(bounds.origin.x + px(150.), bounds.origin.y + px(10.)), + size: size(px(60.), px(40.)), + }, + accent, + )); + }, + ) + .size_full() +} + +// Example 2: Custom Paths with PathBuilder +// +// PathBuilder lets you create complex vector shapes: +// - move_to: Start a new subpath +// - line_to: Draw a straight line +// - curve_to: Draw a bezier curve +// - close: Close the current subpath + +fn create_star(center: Point, outer_radius: f32, inner_radius: f32) -> Path { + let mut builder = PathBuilder::fill(); + let points = 5; + + for i in 0..points * 2 { + let angle = + std::f32::consts::PI / 2.0 - (i as f32) * std::f32::consts::PI / (points as f32); + let radius = if i % 2 == 0 { + outer_radius + } else { + inner_radius + }; + + let x = center.x + px(angle.cos() * radius); + let y = center.y - px(angle.sin() * radius); + + if i == 0 { + builder.move_to(point(x, y)); + } else { + builder.line_to(point(x, y)); + } + } + + builder.close(); + builder.build().unwrap() +} + +fn create_triangle(p1: Point, p2: Point, p3: Point) -> Path { + let mut builder = PathBuilder::fill(); + builder.move_to(p1); + builder.line_to(p2); + builder.line_to(p3); + builder.close(); + builder.build().unwrap() +} + +fn custom_paths_canvas(colors: &Colors) -> impl IntoElement { + let warning = colors.warning; + let accent = colors.accent; + + canvas( + move |_bounds, _window, _cx| {}, + move |bounds, _, window, _cx| { + let center_y = bounds.origin.y + bounds.size.height / 2.0; + + // Draw a star + let star_center = point(bounds.origin.x + px(50.), center_y); + let star = create_star(star_center, 30., 15.); + window.paint_path(star, warning); + + // Draw a triangle + let tri_base_x = bounds.origin.x + px(120.); + let triangle = create_triangle( + point(tri_base_x + px(30.), center_y - px(25.)), + point(tri_base_x, center_y + px(25.)), + point(tri_base_x + px(60.), center_y + px(25.)), + ); + window.paint_path(triangle, rgb(0x8b5cf6)); // Purple + + // Draw a custom shape (arrow) + let arrow_x = bounds.origin.x + px(200.); + let mut arrow_builder = PathBuilder::fill(); + arrow_builder.move_to(point(arrow_x, center_y)); + arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(20.))); + arrow_builder.line_to(point(arrow_x + px(20.), center_y - px(10.))); + arrow_builder.line_to(point(arrow_x + px(50.), center_y - px(10.))); + arrow_builder.line_to(point(arrow_x + px(50.), center_y + px(10.))); + arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(10.))); + arrow_builder.line_to(point(arrow_x + px(20.), center_y + px(20.))); + arrow_builder.close(); + let arrow = arrow_builder.build().unwrap(); + window.paint_path(arrow, accent); + }, + ) + .size_full() +} + +// Example 3: Interactive Drawing +// +// Combine canvas with mouse events for interactive drawing + +struct DrawingCanvas { + lines: Vec>>, + current_line: Vec>, + is_drawing: bool, + color_index: usize, +} + +impl DrawingCanvas { + fn new() -> Self { + Self { + lines: Vec::new(), + current_line: Vec::new(), + is_drawing: false, + color_index: 0, + } + } + + fn get_colors(colors: &Colors) -> Vec { + vec![ + colors.error, + colors.success, + colors.accent, + colors.warning, + rgb(0x8b5cf6), // Purple + rgb(0x06b6d4), // Cyan + ] + } + + fn current_color(&self, colors: &Colors) -> Rgba { + let palette = Self::get_colors(colors); + palette[self.color_index % palette.len()] + } + + fn next_color(&mut self, colors: &Colors) { + let palette = Self::get_colors(colors); + self.color_index = (self.color_index + 1) % palette.len(); + } + + fn on_mouse_down( + &mut self, + event: &MouseDownEvent, + _window: &mut Window, + cx: &mut Context, + ) { + if event.button == MouseButton::Left { + self.is_drawing = true; + self.current_line = vec![event.position]; + cx.notify(); + } + } + + fn on_mouse_move( + &mut self, + event: &MouseMoveEvent, + _window: &mut Window, + cx: &mut Context, + ) { + if self.is_drawing { + self.current_line.push(event.position); + cx.notify(); + } + } + + fn on_mouse_up(&mut self, _event: &MouseUpEvent, window: &mut Window, cx: &mut Context) { + if self.is_drawing && self.current_line.len() > 1 { + self.lines.push(std::mem::take(&mut self.current_line)); + let colors = Colors::for_appearance(window); + self.next_color(&colors); + } + self.is_drawing = false; + self.current_line.clear(); + cx.notify(); + } + + fn clear(&mut self, cx: &mut Context) { + self.lines.clear(); + self.current_line.clear(); + self.color_index = 0; + cx.notify(); + } + + fn draw_line(window: &mut Window, points: &[Point], color: Rgba) { + if points.len() < 2 { + return; + } + + for pair in points.windows(2) { + let start = pair[0]; + let end = pair[1]; + + let dx = end.x - start.x; + let dy = end.y - start.y; + + let dx_f = f32::from(dx); + let dy_f = f32::from(dy); + let len = (dx_f * dx_f + dy_f * dy_f).sqrt(); + + if len < 0.1 { + continue; + } + + let thickness = 3.0_f32; + let px_offset = px(-dy_f / len * thickness / 2.0); + let py_offset = px(dx_f / len * thickness / 2.0); + + let mut builder = PathBuilder::fill(); + builder.move_to(point(start.x + px_offset, start.y + py_offset)); + builder.line_to(point(end.x + px_offset, end.y + py_offset)); + builder.line_to(point(end.x - px_offset, end.y - py_offset)); + builder.line_to(point(start.x - px_offset, start.y - py_offset)); + builder.close(); + + if let Ok(path) = builder.build() { + window.paint_path(path, color); + } + } + } +} + +impl Render for DrawingCanvas { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let lines = self.lines.clone(); + let current_line = self.current_line.clone(); + let current_color = self.current_color(&colors); + let palette = Self::get_colors(&colors); + + let surface = colors.surface; + let border = colors.border; + let error = colors.error; + let error_hover = colors.error_hover; + let text = colors.selected_text; + let text_muted = colors.text_muted; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .id("drawing-area") + .h_48() + .rounded_lg() + .bg(surface) + .border_1() + .border_color(border) + .cursor_crosshair() + .overflow_hidden() + .on_mouse_down(MouseButton::Left, cx.listener(Self::on_mouse_down)) + .on_mouse_move(cx.listener(Self::on_mouse_move)) + .on_mouse_up(MouseButton::Left, cx.listener(Self::on_mouse_up)) + .child( + canvas( + move |_, _, _| {}, + move |_bounds, _, window, _cx| { + for (i, line) in lines.iter().enumerate() { + let color = palette[i % palette.len()]; + DrawingCanvas::draw_line(window, line, color); + } + + if !current_line.is_empty() { + DrawingCanvas::draw_line(window, ¤t_line, current_color); + } + }, + ) + .size_full(), + ), + ) + .child( + div() + .flex() + .gap_2() + .child( + div() + .id("clear-btn") + .px_3() + .py_1() + .rounded_md() + .bg(error) + .text_sm() + .text_color(text) + .cursor_pointer() + .hover(move |s| s.bg(error_hover)) + .child("Clear") + .on_click(cx.listener(|this, _, _, cx| { + this.clear(cx); + })), + ) + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Click and drag to draw"), + ), + ) + } +} + +// Main Application View + +struct CustomDrawingExample { + drawing_canvas: gpui::Entity, +} + +impl CustomDrawingExample { + fn new(cx: &mut Context) -> Self { + Self { + drawing_canvas: cx.new(|_| DrawingCanvas::new()), + } + } +} + +impl Render for CustomDrawingExample { + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(500.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Custom Drawing"), + ) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Canvas element, paths, and interactive painting"), + ), + ) + .child(section( + &colors, + "1. Basic Shapes (paint_quad)", + "Use window.paint_quad() to draw filled rectangles", + basic_shapes_canvas(&colors), + px(70.), + )) + .child(section( + &colors, + "2. Custom Paths (PathBuilder)", + "Create complex shapes with PathBuilder and paint_path()", + custom_paths_canvas(&colors), + px(80.), + )) + .child(section( + &colors, + "3. Interactive Drawing", + "Combine canvas with mouse events for drawing", + self.drawing_canvas.clone(), + px(240.), + )), + ) + } +} + +fn section( + colors: &Colors, + title: &'static str, + description: &'static str, + content: impl IntoElement, + height: Pixels, +) -> impl IntoElement { + let surface: Hsla = colors.surface.into(); + + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(surface.opacity(0.5)) + .border_1() + .border_color(colors.border) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child(description), + ), + ) + .child(div().h(height).child(content)) +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| CustomDrawingExample::new(cx)), + ) + .expect("Failed to open window"); + + example_prelude::init_example(cx, "Custom Drawing"); + }); +} diff --git a/examples/learn/interactive_elements.rs b/examples/learn/interactive_elements.rs new file mode 100644 index 0000000000..22aadbcf61 --- /dev/null +++ b/examples/learn/interactive_elements.rs @@ -0,0 +1,567 @@ +//! Interactive Elements Example +//! +//! This example demonstrates interactive patterns in GPUI: +//! +//! 1. Click events - single click, double click, click count +//! 2. Hover states - hover styling and on_hover callbacks +//! 3. Mouse events - mouse down, mouse up, mouse move +//! 4. Drag and drop - draggable elements and drop targets + +#[path = "../prelude.rs"] +mod example_prelude; + +use example_prelude::init_example; +use gpui::{ + App, Application, Bounds, ClickEvent, Colors, Context, Entity, Half, Hsla, IntoElement, + MouseButton, MouseMoveEvent, Pixels, Point, Render, Window, WindowBounds, WindowOptions, div, + prelude::*, px, size, +}; + +// ============================================================================ +// Click Events Demo +// ============================================================================ +// +// Demonstrates different click interactions: +// - Single click +// - Double click (click_count == 2) +// - Click count tracking + +struct ClickDemo { + click_count: usize, + last_click_type: String, +} + +impl ClickDemo { + fn new() -> Self { + Self { + click_count: 0, + last_click_type: "None".to_string(), + } + } +} + +impl Render for ClickDemo { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.surface) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Click Events"), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("Single click, double click, or triple click the button"), + ) + .child( + div() + .id("click-target") + .px_4() + .py_2() + .rounded_md() + .bg(colors.accent) + .text_color(colors.selected_text) + .text_sm() + .cursor_pointer() + .hover(|style| style.bg(colors.accent_hover)) + .active(|style| style.bg(colors.accent_active)) + .child("Click Me!") + // on_click receives a ClickEvent with click_count() method + .on_click(cx.listener(|this, event: &ClickEvent, _window, cx| { + this.click_count += 1; + this.last_click_type = match event.click_count() { + 1 => "Single Click".to_string(), + 2 => "Double Click".to_string(), + 3 => "Triple Click".to_string(), + n => format!("{n}x Click"), + }; + cx.notify(); + })), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .mt_2() + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child(format!("Total clicks: {}", self.click_count)), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child(format!("Last: {}", self.last_click_type)), + ), + ) + } +} + +// ============================================================================ +// Hover Demo +// ============================================================================ +// +// Demonstrates hover interactions: +// - hover() style modifier for CSS-like hover states +// - on_hover() callback for programmatic hover detection + +struct HoverDemo { + is_hovered: bool, + hover_count: usize, +} + +impl HoverDemo { + fn new() -> Self { + Self { + is_hovered: false, + hover_count: 0, + } + } +} + +impl Render for HoverDemo { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let is_hovered = self.is_hovered; + + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.surface) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Hover Events"), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("Move your mouse in and out of the target"), + ) + .child( + div() + .id("hover-target") + .px_4() + .py_3() + .rounded_md() + .border_2() + .border_color(if is_hovered { + colors.accent + } else { + colors.border + }) + .bg(if is_hovered { + colors.accent_hover + } else { + colors.surface_hover + }) + .text_color(if is_hovered { + colors.selected_text + } else { + colors.text + }) + .text_sm() + .cursor_pointer() + .child(if is_hovered { + "Mouse Inside!" + } else { + "Hover Over Me" + }) + // on_hover callback receives a bool: true when mouse enters, false when it leaves + .on_hover(cx.listener(|this, &hovered, _window, cx| { + this.is_hovered = hovered; + if hovered { + this.hover_count += 1; + } + cx.notify(); + })), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .mt_2() + .child(format!("Times hovered: {}", self.hover_count)), + ) + } +} + +// ============================================================================ +// Mouse Events Demo +// ============================================================================ +// +// Demonstrates low-level mouse events: +// - on_mouse_down - fires when mouse button is pressed +// - on_mouse_up - fires when mouse button is released +// - on_mouse_move - fires when mouse moves over element + +struct MouseEventsDemo { + mouse_position: Option>, + is_pressed: bool, + event_log: Vec, +} + +impl MouseEventsDemo { + fn new() -> Self { + Self { + mouse_position: None, + is_pressed: false, + event_log: Vec::new(), + } + } + + fn log_event(&mut self, event: &str) { + self.event_log.push(event.to_string()); + if self.event_log.len() > 5 { + self.event_log.remove(0); + } + } +} + +impl Render for MouseEventsDemo { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let is_pressed = self.is_pressed; + let position_text = self + .mouse_position + .map(|p| format!("({:.0}, {:.0})", f32::from(p.x), f32::from(p.y))) + .unwrap_or_else(|| "—".to_string()); + + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.surface) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Mouse Events"), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("Move and click within the target area"), + ) + .child( + div() + .id("mouse-events-target") + .h_20() + .rounded_md() + .border_2() + .border_color(if is_pressed { + colors.accent + } else { + colors.border + }) + .bg(if is_pressed { + colors.accent_hover + } else { + colors.surface_hover + }) + .flex() + .items_center() + .justify_center() + .text_sm() + .text_color(colors.text) + .child(format!("Position: {}", position_text)) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _event, _window, cx| { + this.is_pressed = true; + this.log_event("Mouse Down"); + cx.notify(); + }), + ) + .on_mouse_up( + MouseButton::Left, + cx.listener(|this, _event, _window, cx| { + this.is_pressed = false; + this.log_event("Mouse Up"); + cx.notify(); + }), + ) + .on_mouse_move(cx.listener(|this, event: &MouseMoveEvent, _window, cx| { + this.mouse_position = Some(event.position); + cx.notify(); + })), + ) + .child( + div() + .flex() + .flex_col() + .gap_0p5() + .mt_2() + .text_xs() + .text_color(colors.text_muted) + .children( + self.event_log + .iter() + .map(|e| div().child(format!("• {}", e))), + ), + ) + } +} + +// ============================================================================ +// Drag and Drop Demo +// ============================================================================ +// +// Demonstrates drag and drop: +// - on_drag - makes an element draggable, provides drag data +// - on_drop - makes an element a drop target, receives drag data + +#[derive(Clone, Copy)] +struct DragData { + index: usize, + color: Hsla, + position: Point, +} + +impl DragData { + fn new(index: usize, color: Hsla) -> Self { + Self { + index, + color, + position: Point::default(), + } + } + + fn with_position(mut self, position: Point) -> Self { + self.position = position; + self + } +} + +// Render trait for DragData allows it to be rendered as drag feedback +impl Render for DragData { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let size = gpui::size(px(80.), px(40.)); + + // Position the drag preview at the cursor + div() + .pl(self.position.x - size.width.half()) + .pt(self.position.y - size.height.half()) + .child( + div() + .flex() + .justify_center() + .items_center() + .w(size.width) + .h(size.height) + .bg(self.color.opacity(0.8)) + .text_color(gpui::white()) + .text_xs() + .rounded_md() + .shadow_lg() + .child(format!("Item {}", self.index + 1)), + ) + } +} + +struct DragDropDemo { + dropped_item: Option, +} + +impl DragDropDemo { + fn new() -> Self { + Self { dropped_item: None } + } +} + +impl Render for DragDropDemo { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + let item_colors = [colors.error, colors.success, colors.warning]; + + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .rounded_lg() + .bg(colors.surface) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child("Drag and Drop"), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("Drag items to the drop zone below"), + ) + .child( + div() + .flex() + .gap_2() + .children(item_colors.into_iter().enumerate().map(|(index, color)| { + let drag_data = DragData::new(index, color.into()); + + div() + .id(("drag-item", index)) + .px_3() + .py_2() + .rounded_md() + .border_2() + .border_color(color) + .text_color(color) + .text_xs() + .cursor_grab() + .hover(move |style| { + let c: Hsla = color.into(); + style.bg(c.opacity(0.1)) + }) + .child(format!("Item {}", index + 1)) + // on_drag takes: drag data, and a closure that creates the drag preview + .on_drag(drag_data, |data: &DragData, position, _, cx| { + cx.new(|_| data.with_position(position)) + }) + })), + ) + .child( + div() + .id("drop-target") + .mt_2() + .h_16() + .rounded_md() + .border_2() + .border_dashed() + .border_color( + self.dropped_item + .map(|d| d.color) + .unwrap_or_else(|| colors.border.into()), + ) + .when_some(self.dropped_item, |el, data| el.bg(data.color.opacity(0.2))) + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(colors.text_muted) + // on_drop receives the drag data when an item is dropped + .on_drop(cx.listener(|this, data: &DragData, _window, cx| { + this.dropped_item = Some(*data); + cx.notify(); + })) + .child( + self.dropped_item + .map(|d| format!("Dropped: Item {}", d.index + 1)) + .unwrap_or_else(|| "Drop Zone".to_string()), + ), + ) + } +} + +// ============================================================================ +// Main Application +// ============================================================================ + +struct InteractiveElementsExample { + click_demo: Entity, + hover_demo: Entity, + mouse_events_demo: Entity, + drag_drop_demo: Entity, +} + +impl InteractiveElementsExample { + fn new(cx: &mut Context) -> Self { + Self { + click_demo: cx.new(|_| ClickDemo::new()), + hover_demo: cx.new(|_| HoverDemo::new()), + mouse_events_demo: cx.new(|_| MouseEventsDemo::new()), + drag_drop_demo: cx.new(|_| DragDropDemo::new()), + } + } +} + +impl Render for InteractiveElementsExample { + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div() + .id("main") + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(800.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Interactive Elements"), + ) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Click, hover, mouse events, and drag-and-drop in GPUI"), + ), + ) + .child( + div() + .grid() + .grid_cols(2) + .gap_4() + .child(self.click_demo.clone()) + .child(self.hover_demo.clone()) + .child(self.mouse_events_demo.clone()) + .child(self.drag_drop_demo.clone()), + ), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(700.), px(650.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|cx| InteractiveElementsExample::new(cx)), + ) + .expect("Failed to open window"); + + init_example(cx, "Interactive Elements"); + }); +} diff --git a/examples/learn/layout.rs b/examples/learn/layout.rs new file mode 100644 index 0000000000..909a0c8d91 --- /dev/null +++ b/examples/learn/layout.rs @@ -0,0 +1,499 @@ +//! Layout Patterns Example +//! +//! This example demonstrates different layout approaches in GPUI: +//! +//! 1. Flexbox - Row and column layouts with alignment +//! 2. Grid - Two-dimensional layouts with spans +//! 3. Common patterns - Sidebar, header/footer, centering + +#[path = "../prelude.rs"] +mod example_prelude; + +use example_prelude::init_example; +use gpui::{ + App, Application, Bounds, Colors, Context, Div, Hsla, Render, Rgba, Window, WindowBounds, + WindowOptions, div, prelude::*, px, size, +}; + +// Helper: Colored block for visualization + +fn block(label: &'static str, color: Hsla, text_color: Rgba) -> Div { + div() + .flex() + .items_center() + .justify_center() + .bg(color) + .border_1() + .border_color(gpui::white().opacity(0.3)) + .rounded_md() + .text_xs() + .text_color(text_color) + .child(label) +} + +// Flexbox Examples + +fn flexbox_row_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.selected_text; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("flex().flex_row().gap_2()"), + ) + .child( + div() + .flex() + .flex_row() + .gap_2() + .child(block("A", gpui::red(), text).size_8()) + .child(block("B", gpui::green(), text).size_8()) + .child(block("C", gpui::blue(), text).size_8()), + ) +} + +fn flexbox_column_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.selected_text; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("flex().flex_col().gap_2()"), + ) + .child( + div() + .h_24() + .flex() + .flex_col() + .gap_2() + .child(block("A", gpui::red(), text).h_6()) + .child(block("B", gpui::green(), text).h_6()) + .child(block("C", gpui::blue(), text).h_6()), + ) +} + +fn flexbox_justify_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.selected_text; + let surface = colors.surface; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("justify_between / justify_center / justify_end"), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .flex() + .justify_between() + .p_1() + .bg(surface) + .rounded_sm() + .child(block("Start", gpui::red(), text).px_2().py_1()) + .child(block("End", gpui::blue(), text).px_2().py_1()), + ) + .child( + div() + .flex() + .justify_center() + .p_1() + .bg(surface) + .rounded_sm() + .child(block("Center", gpui::green(), text).px_2().py_1()), + ) + .child( + div() + .flex() + .justify_end() + .p_1() + .bg(surface) + .rounded_sm() + .child(block("End", gpui::yellow(), text).px_2().py_1()), + ), + ) +} + +fn flexbox_grow_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.selected_text; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("flex_1 (grow) vs flex_none (fixed)"), + ) + .child( + div() + .flex() + .gap_2() + .child(block("fixed", gpui::red(), text).flex_none().w_16().h_8()) + .child(block("flex_1 (grows)", gpui::green(), text).flex_1().h_8()) + .child(block("fixed", gpui::blue(), text).flex_none().w_16().h_8()), + ) +} + +// Grid Examples + +fn grid_basic_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.selected_text; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("grid().grid_cols(3).gap_1()"), + ) + .child( + div() + .grid() + .grid_cols(3) + .gap_1() + .child(block("1", gpui::red(), text).h_8()) + .child(block("2", gpui::green(), text).h_8()) + .child(block("3", gpui::blue(), text).h_8()) + .child(block("4", gpui::yellow(), text).h_8()) + .child(block("5", gpui::red(), text).h_8()) + .child(block("6", gpui::green(), text).h_8()), + ) +} + +fn grid_span_example(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.selected_text; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("col_span / row_span"), + ) + .child( + div() + .grid() + .grid_cols(4) + .grid_rows(3) + .gap_1() + .child( + block("Header (col_span_full)", gpui::red(), text) + .col_span_full() + .h_6(), + ) + .child( + block("Side", gpui::green(), text) + .col_span(1) + .row_span(2) + .h_full(), + ) + .child( + block("Content (col_span 3)", gpui::blue(), text) + .col_span(3) + .row_span(2) + .h_full(), + ), + ) +} + +// Common Layout Patterns + +fn app_shell_pattern(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.text; + let surface = colors.surface; + let surface_hover = colors.surface_hover; + let background = colors.background; + let border = colors.border; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("App Shell: Header + Sidebar + Content"), + ) + .child( + div() + .h_32() + .flex() + .flex_col() + .border_1() + .border_color(border) + .rounded_md() + .overflow_hidden() + .child( + div() + .h_6() + .flex() + .items_center() + .px_2() + .bg(surface_hover) + .text_xs() + .text_color(text) + .child("Header"), + ) + .child( + div() + .flex_1() + .flex() + .child( + div() + .w_16() + .bg(surface) + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(text_muted) + .child("Side"), + ) + .child( + div() + .flex_1() + .bg(background) + .flex() + .items_center() + .justify_center() + .text_xs() + .text_color(text_muted) + .child("Content"), + ), + ), + ) +} + +fn centered_pattern(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let text = colors.selected_text; + let surface = colors.surface; + let accent = colors.accent; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Centering: items_center + justify_center"), + ) + .child( + div() + .h_20() + .flex() + .items_center() + .justify_center() + .bg(surface) + .rounded_md() + .child( + div() + .px_4() + .py_2() + .bg(accent) + .rounded_md() + .text_xs() + .text_color(text) + .child("Perfectly Centered"), + ), + ) +} + +fn stack_pattern(colors: &Colors) -> impl IntoElement { + let text_muted = colors.text_muted; + let surface = colors.surface; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Stack: Overlapping with absolute positioning"), + ) + .child( + div() + .h_20() + .relative() + .bg(surface) + .rounded_md() + .child( + div() + .absolute() + .top_2() + .left_2() + .size_10() + .bg(gpui::red().opacity(0.7)) + .rounded_md(), + ) + .child( + div() + .absolute() + .top_4() + .left_4() + .size_10() + .bg(gpui::green().opacity(0.7)) + .rounded_md(), + ) + .child( + div() + .absolute() + .top_6() + .left_6() + .size_10() + .bg(gpui::blue().opacity(0.7)) + .rounded_md(), + ), + ) +} + +// Main Application View + +struct LayoutExample; + +impl Render for LayoutExample { + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div() + .id("main") + .size_full() + .p_4() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_4() + .max_w(px(600.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Layout Patterns"), + ) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Flexbox, Grid, and common layout patterns in GPUI"), + ), + ) + .child(section( + &colors, + "Flexbox: Row", + flexbox_row_example(&colors), + )) + .child(section( + &colors, + "Flexbox: Column", + flexbox_column_example(&colors), + )) + .child(section( + &colors, + "Flexbox: Justify Content", + flexbox_justify_example(&colors), + )) + .child(section( + &colors, + "Flexbox: Grow/Shrink", + flexbox_grow_example(&colors), + )) + .child(section(&colors, "Grid: Basic", grid_basic_example(&colors))) + .child(section(&colors, "Grid: Spans", grid_span_example(&colors))) + .child(section( + &colors, + "Pattern: App Shell", + app_shell_pattern(&colors), + )) + .child(section( + &colors, + "Pattern: Centering", + centered_pattern(&colors), + )) + .child(section(&colors, "Pattern: Stack", stack_pattern(&colors))), + ) + } +} + +fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { + let surface: Hsla = colors.surface.into(); + + div() + .flex() + .flex_col() + .gap_2() + .p_3() + .bg(surface.opacity(0.5)) + .rounded_lg() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(650.), px(700.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| LayoutExample), + ) + .expect("Failed to open window"); + + init_example(cx, "Layout"); + }); +} diff --git a/examples/learn/styling.rs b/examples/learn/styling.rs new file mode 100644 index 0000000000..f3475d6561 --- /dev/null +++ b/examples/learn/styling.rs @@ -0,0 +1,511 @@ +//! Styling Patterns Example +//! +//! This example demonstrates different styling approaches in GPUI: +//! +//! 1. Interactive states - hover, active, focus, focus_visible +//! 2. Conditional styling - when, when_some, map +//! 3. Theming patterns - using Colors for consistent styling + +use gpui::{ + App, Application, Bounds, Colors, Context, FocusHandle, Hsla, KeyBinding, Menu, MenuItem, + Render, Rgba, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, rgb, size, +}; + +actions!(styling_example, [Quit, Tab, TabPrev]); + +// Interactive States Example + +fn interactive_button( + id: impl Into, + label: &'static str, + colors: &Colors, +) -> impl IntoElement { + let accent = colors.accent; + let accent_hover = colors.accent_hover; + let accent_active = colors.accent_active; + let text = colors.selected_text; + + div() + .id(id) + .px_4() + .py_2() + .rounded_md() + .cursor_pointer() + .bg(accent) + .text_color(text) + .text_sm() + .hover(move |style| style.bg(accent_hover)) + .active(move |style| style.bg(accent_active)) + .child(label) +} + +fn focus_button( + id: impl Into, + label: &'static str, + focus_handle: &FocusHandle, + colors: &Colors, +) -> impl IntoElement { + let surface = colors.surface; + let surface_hover = colors.surface_hover; + let text = colors.text; + let accent = colors.accent; + let focus_ring: Rgba = rgb(0x60a5fa); + + div() + .id(id) + .track_focus(focus_handle) + .px_4() + .py_2() + .rounded_md() + .cursor_pointer() + .bg(surface) + .text_color(text) + .text_sm() + .border_2() + .border_color(gpui::transparent_black()) + .hover(move |style| style.bg(surface_hover)) + .focus(move |style| style.border_color(accent)) + .focus_visible(move |style| style.border_color(focus_ring).shadow_sm()) + .child(label) +} + +fn interactive_states_section(colors: &Colors) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("hover() / active() - Mouse interaction states"), + ) + .child( + div() + .flex() + .gap_2() + .child(interactive_button("btn-1", "Hover me", colors)) + .child(interactive_button("btn-2", "Click me", colors)), + ) +} + +// Conditional Styling Example + +fn status_badge(status: &'static str, variant: StatusVariant, colors: &Colors) -> impl IntoElement { + let (bg, text): (Rgba, Rgba) = match variant { + StatusVariant::Success => (colors.success, colors.selected_text), + StatusVariant::Warning => (colors.warning, rgb(0x000000)), + StatusVariant::Error => (colors.error, colors.selected_text), + StatusVariant::Neutral => (colors.surface, colors.text), + }; + + div() + .px_2() + .py_0p5() + .rounded_full() + .text_xs() + .bg(bg) + .text_color(text) + .child(status) +} + +#[derive(Clone, Copy)] +enum StatusVariant { + Success, + Warning, + Error, + Neutral, +} + +fn list_item( + id: impl Into, + label: &'static str, + is_selected: bool, + is_disabled: bool, + colors: &Colors, +) -> impl IntoElement { + let surface = colors.surface; + let surface_hover = colors.surface_hover; + let text = colors.text; + let text_muted = colors.text_muted; + let accent = colors.accent; + + div() + .id(id) + .px_3() + .py_2() + .rounded_md() + .text_sm() + .cursor_pointer() + .border_1() + .border_color(gpui::transparent_black()) + .when(is_disabled, |el| { + el.opacity(0.5) + .cursor_not_allowed() + .bg(surface) + .text_color(text_muted) + }) + .when(!is_disabled && is_selected, move |el| { + let accent_bg: Hsla = accent.into(); + el.bg(accent_bg.opacity(0.2)) + .border_color(accent) + .text_color(text) + }) + .when(!is_disabled && !is_selected, move |el| { + el.bg(surface) + .text_color(text) + .hover(move |style| style.bg(surface_hover)) + }) + .child(label) +} + +fn conditional_section(colors: &Colors) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("when() - Apply styles conditionally"), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child(list_item("item-1", "Normal item", false, false, colors)) + .child(list_item("item-2", "Selected item", true, false, colors)) + .child(list_item("item-3", "Disabled item", false, true, colors)), + ) + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .mt_2() + .child("Status badges with variant-based styling"), + ) + .child( + div() + .flex() + .gap_2() + .child(status_badge("Success", StatusVariant::Success, colors)) + .child(status_badge("Warning", StatusVariant::Warning, colors)) + .child(status_badge("Error", StatusVariant::Error, colors)) + .child(status_badge("Neutral", StatusVariant::Neutral, colors)), + ) +} + +// Group Hover Example + +fn card_with_group_hover( + id: impl Into, + title: &'static str, + description: &'static str, + colors: &Colors, +) -> impl IntoElement { + let surface = colors.surface; + let border = colors.border; + let accent = colors.accent; + let text = colors.text; + let text_muted = colors.text_muted; + + div() + .id(id) + .group("card") + .p_4() + .rounded_lg() + .bg(surface) + .border_1() + .border_color(border) + .cursor_pointer() + .hover(move |style| style.border_color(accent)) + .child( + div() + .flex() + .justify_between() + .items_center() + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(text) + .child(title), + ) + .child( + div() + .text_xs() + .text_color(text_muted) + .opacity(0.) + .group_hover("card", |style| style.opacity(1.)) + .child("→"), + ), + ) + .child( + div() + .mt_1() + .text_xs() + .text_color(text_muted) + .child(description), + ) +} + +fn group_hover_section(colors: &Colors) -> impl IntoElement { + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("group() / group_hover() - Parent hover affects children"), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child(card_with_group_hover( + "card-1", + "Documents", + "View and manage your documents", + colors, + )) + .child(card_with_group_hover( + "card-2", + "Settings", + "Configure application settings", + colors, + )), + ) +} + +// Main Application View + +struct StylingExample { + focus_handle: FocusHandle, + buttons: Vec, +} + +impl StylingExample { + fn new(window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + window.focus(&focus_handle); + + let buttons = vec![ + cx.focus_handle().tab_index(1).tab_stop(true), + cx.focus_handle().tab_index(2).tab_stop(true), + cx.focus_handle().tab_index(3).tab_stop(true), + ]; + + Self { + focus_handle, + buttons, + } + } + + fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context) { + window.focus_next(); + } + + fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context) { + window.focus_prev(); + } +} + +impl Render for StylingExample { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div() + .id("app") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::on_tab)) + .on_action(cx.listener(Self::on_tab_prev)) + .size_full() + .p_6() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_6() + .max_w(px(500.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(gpui::FontWeight::BOLD) + .text_color(colors.text) + .child("Styling Patterns"), + ) + .child( + div() + .text_sm() + .text_color(colors.text_muted) + .child("Interactive states, conditional styling, and theming"), + ), + ) + .child(section( + &colors, + "Interactive States", + interactive_states_section(&colors), + )) + .child(section( + &colors, + "Focus States (Tab to navigate)", + div() + .flex() + .flex_col() + .gap_3() + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("focus() / focus_visible() - Keyboard navigation"), + ) + .child( + div() + .flex() + .gap_2() + .child(focus_button( + "focus-1", + "Button 1", + &self.buttons[0], + &colors, + )) + .child(focus_button( + "focus-2", + "Button 2", + &self.buttons[1], + &colors, + )) + .child(focus_button( + "focus-3", + "Button 3", + &self.buttons[2], + &colors, + )), + ), + )) + .child(section( + &colors, + "Conditional Styling", + conditional_section(&colors), + )) + .child(section( + &colors, + "Group Hover", + group_hover_section(&colors), + )) + .child(section( + &colors, + "Default Colors", + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(colors.text_muted) + .child("Using Colors::for_appearance() for consistent theming"), + ) + .child( + div() + .flex() + .flex_wrap() + .gap_2() + .child(color_swatch(&colors, "background", colors.background)) + .child(color_swatch(&colors, "surface", colors.surface)) + .child(color_swatch(&colors, "accent", colors.accent)) + .child(color_swatch(&colors, "success", colors.success)) + .child(color_swatch(&colors, "warning", colors.warning)) + .child(color_swatch(&colors, "error", colors.error)) + .child(color_swatch(&colors, "border", colors.border)), + ), + )), + ) + } +} + +fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { + let surface: Hsla = colors.surface.into(); + let border: Hsla = colors.border.into(); + + div() + .flex() + .flex_col() + .gap_3() + .p_4() + .bg(surface.opacity(0.3)) + .rounded_lg() + .border_1() + .border_color(border.opacity(0.5)) + .child( + div() + .text_sm() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) +} + +fn color_swatch(colors: &Colors, name: &'static str, color: Rgba) -> impl IntoElement { + let text_muted = colors.text_muted; + + div() + .flex() + .flex_col() + .items_center() + .gap_1() + .child( + div() + .size_8() + .rounded_md() + .bg(color) + .border_1() + .border_color(gpui::white().opacity(0.2)), + ) + .child(div().text_xs().text_color(text_muted).child(name)) +} + +fn main() { + Application::new().run(|cx: &mut App| { + cx.activate(true); + cx.on_action(|_: &Quit, cx| cx.quit()); + cx.bind_keys([ + KeyBinding::new("cmd-q", Quit, None), + KeyBinding::new("tab", Tab, None), + KeyBinding::new("shift-tab", TabPrev, None), + ]); + cx.set_menus(vec![Menu { + name: "Styling".into(), + items: vec![MenuItem::action("Quit", Quit)], + }]); + cx.on_window_closed(|cx| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); + + let bounds = Bounds::centered(None, size(px(550.), px(800.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| StylingExample::new(window, cx)), + ) + .expect("Failed to open window"); + }); +} diff --git a/examples/learn/text.rs b/examples/learn/text.rs new file mode 100644 index 0000000000..5f938dc5ea --- /dev/null +++ b/examples/learn/text.rs @@ -0,0 +1,582 @@ +//! Text Example +//! +//! This example demonstrates text capabilities in GPUI: +//! +//! 1. Text Styling - Font sizes, weights, and colors +//! 2. Text Alignment - Left, center, right alignment +//! 3. Text Decoration - Underline, strikethrough +//! 4. Text Overflow - Ellipsis, truncation, line clamping +//! 5. Styled Text - Inline style variations with highlights +//! 6. Character Grid - Unicode and emoji support + +#[path = "../prelude.rs"] +mod example_prelude; + +use example_prelude::init_example; +use gpui::{ + App, Application, Bounds, Colors, Context, FontStyle, FontWeight, Hsla, Render, StyledText, + TextOverflow, Window, WindowBounds, WindowOptions, div, prelude::*, px, relative, size, +}; + +// Text Styling Examples + +fn text_sizes_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Font sizes: text_xs, text_sm, text_base, text_lg, text_xl"), + ) + .child( + div() + .flex() + .flex_wrap() + .items_baseline() + .gap_3() + .child(div().text_xs().text_color(text).child("Extra Small")) + .child(div().text_sm().text_color(text).child("Small")) + .child(div().text_base().text_color(text).child("Base")) + .child(div().text_lg().text_color(text).child("Large")) + .child(div().text_xl().text_color(text).child("Extra Large")), + ) +} + +fn text_weights_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Font weights: THIN through BLACK"), + ) + .child( + div() + .flex() + .flex_wrap() + .gap_3() + .child( + div() + .text_color(text) + .font_weight(FontWeight::THIN) + .child("Thin"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::LIGHT) + .child("Light"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::NORMAL) + .child("Normal"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::MEDIUM) + .child("Medium"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::SEMIBOLD) + .child("Semibold"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::BOLD) + .child("Bold"), + ) + .child( + div() + .text_color(text) + .font_weight(FontWeight::BLACK) + .child("Black"), + ), + ) +} + +// Text Alignment Examples + +fn text_alignment_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + let surface = colors.surface; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Alignment: default (left), text_center, text_right"), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .child("Left aligned (default)"), + ) + .child( + div() + .p_2() + .bg(surface) + .rounded_sm() + .text_center() + .text_color(text) + .child("Center aligned"), + ) + .child( + div() + .p_2() + .bg(surface) + .rounded_sm() + .text_right() + .text_color(text) + .child("Right aligned"), + ), + ) +} + +// Text Decoration Examples + +fn text_decoration_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + let accent = colors.accent; + let error = colors.error; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Decorations: underline, strikethrough, italic"), + ) + .child( + div() + .flex() + .flex_wrap() + .gap_4() + .child( + div() + .text_color(text) + .text_decoration_1() + .text_decoration_color(accent) + .child("Underlined text"), + ) + .child( + div() + .text_color(text) + .line_through() + .text_decoration_color(error) + .child("Strikethrough text"), + ) + .child(div().text_color(text).italic().child("Italic text")), + ) +} + +// Text Overflow Examples + +fn text_overflow_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + let surface = colors.surface; + let border = colors.border; + + let long_text = "The quick brown fox jumps over the lazy dog. This is a long sentence that will overflow its container."; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Overflow handling: ellipsis, truncate, line_clamp"), + ) + .child( + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("text_ellipsis (single line):"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .text_ellipsis() + .child(long_text), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("line_clamp(2):"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .text_ellipsis() + .line_clamp(2) + .child(long_text), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("truncate (hard cut):"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .text_overflow(TextOverflow::Truncate("".into())) + .child(long_text), + ), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("whitespace_nowrap:"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_sm() + .text_color(text) + .overflow_hidden() + .whitespace_nowrap() + .child(long_text), + ), + ), + ) +} + +// Styled Text Examples + +fn styled_text_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("StyledText with inline highlights"), + ) + .child(div().text_lg().text_color(text).child( + StyledText::new("Bold Italic Normal Semibold").with_highlights([ + (0..4, FontWeight::BOLD.into()), + (5..11, FontStyle::Italic.into()), + (19..27, FontWeight::SEMIBOLD.into()), + ]), + )) +} + +// Character Grid Example + +fn character_grid_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + let surface = colors.surface; + let border = colors.border; + + let characters = [ + // Latin + "A", "B", "C", "D", "E", "a", "b", "c", "d", "e", // Numbers + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", // Greek + "α", "β", "γ", "δ", "ε", "θ", "λ", "π", "σ", "ω", // Cyrillic + "Д", "Ж", "И", "Л", "Ф", "Ц", "Ш", "Щ", "Ы", "Я", // CJK + "你", "好", "世", "界", "日", "本", "語", "中", "文", "字", // Symbols + "→", "←", "↑", "↓", "•", "★", "♠", "♥", "♦", "♣", // Emoji + "😀", "🎉", "🚀", "💡", "🔥", "✨", "🎨", "📚", "🎵", "❤️", + ]; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Unicode and emoji support"), + ) + .child( + div() + .p_2() + .bg(surface) + .border_1() + .border_color(border) + .rounded_md() + .child( + div() + .grid() + .grid_cols(10) + .gap_1() + .children(characters.iter().map(|c| { + div() + .flex() + .items_center() + .justify_center() + .size_8() + .text_lg() + .text_color(text) + .line_height(relative(1.0)) + .child(*c) + })), + ), + ) +} + +// Line Height Example + +fn line_height_example(colors: &Colors) -> impl IntoElement { + let text = colors.text; + let text_muted = colors.text_muted; + let surface = colors.surface; + + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_xs() + .text_color(text_muted) + .child("Line height: relative(1.0), relative(1.5), relative(2.0)"), + ) + .child( + div() + .flex() + .gap_3() + .child( + div() + .flex_1() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .text_sm() + .line_height(relative(1.0)) + .child("Tight\nline\nheight"), + ) + .child( + div() + .flex_1() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .text_sm() + .line_height(relative(1.5)) + .child("Normal\nline\nheight"), + ) + .child( + div() + .flex_1() + .p_2() + .bg(surface) + .rounded_sm() + .text_color(text) + .text_sm() + .line_height(relative(2.0)) + .child("Loose\nline\nheight"), + ), + ) +} + +// Main Application View + +struct TextExample; + +impl Render for TextExample { + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let colors = Colors::for_appearance(window); + + div() + .id("main") + .size_full() + .p_4() + .bg(colors.background) + .overflow_scroll() + .child( + div() + .flex() + .flex_col() + .gap_4() + .max_w(px(600.)) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_xl() + .font_weight(FontWeight::BOLD) + .text_color(colors.text) + .child("Text & Typography"), + ) + .child( + div().text_sm().text_color(colors.text_muted).child( + "Font styling, alignment, overflow, and unicode support", + ), + ), + ) + .child(section(&colors, "Font Sizes", text_sizes_example(&colors))) + .child(section( + &colors, + "Font Weights", + text_weights_example(&colors), + )) + .child(section( + &colors, + "Text Alignment", + text_alignment_example(&colors), + )) + .child(section( + &colors, + "Text Decoration", + text_decoration_example(&colors), + )) + .child(section( + &colors, + "Line Height", + line_height_example(&colors), + )) + .child(section( + &colors, + "Styled Text", + styled_text_example(&colors), + )) + .child(section( + &colors, + "Text Overflow", + text_overflow_example(&colors), + )) + .child(section( + &colors, + "Character Grid", + character_grid_example(&colors), + )), + ) + } +} + +fn section(colors: &Colors, title: &'static str, content: impl IntoElement) -> impl IntoElement { + let surface: Hsla = colors.surface.into(); + + div() + .flex() + .flex_col() + .gap_2() + .p_3() + .bg(surface.opacity(0.5)) + .rounded_lg() + .child( + div() + .text_sm() + .font_weight(FontWeight::SEMIBOLD) + .text_color(colors.text) + .child(title), + ) + .child(content) +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(650.), px(900.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| TextExample), + ) + .expect("Failed to open window"); + + init_example(cx, "Text"); + }); +} diff --git a/crates/gpui/examples/focus_visible.rs b/examples/legacy/focus_visible.rs similarity index 100% rename from crates/gpui/examples/focus_visible.rs rename to examples/legacy/focus_visible.rs diff --git a/crates/gpui/examples/gif_viewer.rs b/examples/legacy/gif_viewer.rs similarity index 100% rename from crates/gpui/examples/gif_viewer.rs rename to examples/legacy/gif_viewer.rs diff --git a/crates/gpui/examples/gradient.rs b/examples/legacy/gradient.rs similarity index 100% rename from crates/gpui/examples/gradient.rs rename to examples/legacy/gradient.rs diff --git a/crates/gpui/examples/hello_world.rs b/examples/legacy/hello_world.rs similarity index 100% rename from crates/gpui/examples/hello_world.rs rename to examples/legacy/hello_world.rs diff --git a/crates/gpui/examples/image/app-icon.png b/examples/legacy/image/app-icon.png similarity index 100% rename from crates/gpui/examples/image/app-icon.png rename to examples/legacy/image/app-icon.png diff --git a/crates/gpui/examples/image/arrow_circle.svg b/examples/legacy/image/arrow_circle.svg similarity index 100% rename from crates/gpui/examples/image/arrow_circle.svg rename to examples/legacy/image/arrow_circle.svg diff --git a/crates/gpui/examples/image/black-cat-typing.gif b/examples/legacy/image/black-cat-typing.gif similarity index 100% rename from crates/gpui/examples/image/black-cat-typing.gif rename to examples/legacy/image/black-cat-typing.gif diff --git a/crates/gpui/examples/image/color.svg b/examples/legacy/image/color.svg similarity index 100% rename from crates/gpui/examples/image/color.svg rename to examples/legacy/image/color.svg diff --git a/crates/gpui/examples/image/image.rs b/examples/legacy/image/image.rs similarity index 100% rename from crates/gpui/examples/image/image.rs rename to examples/legacy/image/image.rs diff --git a/crates/gpui/examples/image_loading.rs b/examples/legacy/image_loading.rs similarity index 100% rename from crates/gpui/examples/image_loading.rs rename to examples/legacy/image_loading.rs diff --git a/crates/gpui/examples/input.rs b/examples/legacy/input.rs similarity index 100% rename from crates/gpui/examples/input.rs rename to examples/legacy/input.rs diff --git a/crates/gpui/examples/layer_shell.rs b/examples/legacy/layer_shell.rs similarity index 100% rename from crates/gpui/examples/layer_shell.rs rename to examples/legacy/layer_shell.rs diff --git a/crates/gpui/examples/on_window_close_quit.rs b/examples/legacy/on_window_close_quit.rs similarity index 100% rename from crates/gpui/examples/on_window_close_quit.rs rename to examples/legacy/on_window_close_quit.rs diff --git a/crates/gpui/examples/opacity.rs b/examples/legacy/opacity.rs similarity index 100% rename from crates/gpui/examples/opacity.rs rename to examples/legacy/opacity.rs diff --git a/crates/gpui/examples/scrollable.rs b/examples/legacy/scrollable.rs similarity index 100% rename from crates/gpui/examples/scrollable.rs rename to examples/legacy/scrollable.rs diff --git a/crates/gpui/examples/svg/dragon.svg b/examples/legacy/svg/dragon.svg similarity index 100% rename from crates/gpui/examples/svg/dragon.svg rename to examples/legacy/svg/dragon.svg diff --git a/crates/gpui/examples/svg/svg.rs b/examples/legacy/svg/svg.rs similarity index 100% rename from crates/gpui/examples/svg/svg.rs rename to examples/legacy/svg/svg.rs diff --git a/crates/gpui/examples/tab_stop.rs b/examples/legacy/tab_stop.rs similarity index 100% rename from crates/gpui/examples/tab_stop.rs rename to examples/legacy/tab_stop.rs diff --git a/crates/gpui/examples/tree.rs b/examples/legacy/tree.rs similarity index 100% rename from crates/gpui/examples/tree.rs rename to examples/legacy/tree.rs diff --git a/crates/gpui/examples/uniform_list.rs b/examples/legacy/uniform_list.rs similarity index 100% rename from crates/gpui/examples/uniform_list.rs rename to examples/legacy/uniform_list.rs diff --git a/crates/gpui/examples/window.rs b/examples/legacy/window.rs similarity index 100% rename from crates/gpui/examples/window.rs rename to examples/legacy/window.rs diff --git a/crates/gpui/examples/window_positioning.rs b/examples/legacy/window_positioning.rs similarity index 100% rename from crates/gpui/examples/window_positioning.rs rename to examples/legacy/window_positioning.rs diff --git a/crates/gpui/examples/window_shadow.rs b/examples/legacy/window_shadow.rs similarity index 100% rename from crates/gpui/examples/window_shadow.rs rename to examples/legacy/window_shadow.rs diff --git a/examples/prelude.rs b/examples/prelude.rs new file mode 100644 index 0000000000..7d9899166f --- /dev/null +++ b/examples/prelude.rs @@ -0,0 +1,41 @@ +//! Example Prelude +//! +//! Common helpers for GPUI examples. Import with: +//! +//! ```ignore +//! #[path = "../prelude.rs"] +//! mod example_prelude; +//! use example_prelude::init_example; +//! ``` + +use gpui::{App, KeyBinding, Menu, MenuItem, SharedString, actions}; + +actions!(example, [Quit, CloseWindow]); + +/// Sets up common example boilerplate: +/// - Activates the application +/// - Sets up an app menu with the example name and a Quit action (cmd-q) +/// - Configures the app to quit when all windows are closed +pub fn init_example(cx: &mut App, name: impl Into) { + // Bring the example window to the front + cx.activate(true); + + // Define the quit action... + cx.on_action(|_: &Quit, cx| cx.quit()); + // ...then bind it to cmd+q + cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); + + // Set up an app menu with the example name and a Quit action (cmd-q) + cx.set_menus(vec![Menu { + name: name.into(), + items: vec![MenuItem::action("Quit", Quit)], + }]); + + // Quit the app when all windows are closed + cx.on_window_closed(|cx| { + if cx.windows().is_empty() { + cx.quit(); + } + }) + .detach(); +} diff --git a/crates/gpui/resources/windows/gpui.manifest.xml b/resources/windows/gpui.manifest.xml similarity index 100% rename from crates/gpui/resources/windows/gpui.manifest.xml rename to resources/windows/gpui.manifest.xml diff --git a/crates/gpui/resources/windows/gpui.rc b/resources/windows/gpui.rc similarity index 100% rename from crates/gpui/resources/windows/gpui.rc rename to resources/windows/gpui.rc diff --git a/script/clippy b/script/clippy index 5c13b0b39c..494ccf81dd 100755 --- a/script/clippy +++ b/script/clippy @@ -2,10 +2,6 @@ set -euo pipefail -if [[ ! " $* " == *" -p "* && ! " $* " == *" --package "* ]]; then - set -- "$@" --workspace -fi - set -x "${CARGO:-cargo}" clippy "$@" --release --all-targets --all-features -- --deny warnings diff --git a/crates/gpui/src/action.rs b/src/action.rs similarity index 100% rename from crates/gpui/src/action.rs rename to src/action.rs diff --git a/crates/gpui/src/app.rs b/src/app.rs similarity index 99% rename from crates/gpui/src/app.rs rename to src/app.rs index f7c57ef015..1faae21cbc 100644 --- a/crates/gpui/src/app.rs +++ b/src/app.rs @@ -42,9 +42,9 @@ use crate::{ PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, WindowHandle, WindowId, - WindowInvalidator, - colors::{Colors, GlobalColors}, - current_platform, hash, init_app_menus, + WindowInvalidator, current_platform, + default_colors::{Colors, GlobalColors}, + hash, init_app_menus, }; mod async_context; @@ -2427,6 +2427,10 @@ pub struct KeystrokeEvent { struct NullHttpClient; impl HttpClient for NullHttpClient { + fn type_name(&self) -> &'static str { + "NullHttpClient" + } + fn send( &self, _req: http_client::Request, diff --git a/crates/gpui/src/app/async_context.rs b/src/app/async_context.rs similarity index 100% rename from crates/gpui/src/app/async_context.rs rename to src/app/async_context.rs diff --git a/crates/gpui/src/app/context.rs b/src/app/context.rs similarity index 100% rename from crates/gpui/src/app/context.rs rename to src/app/context.rs diff --git a/crates/gpui/src/app/entity_map.rs b/src/app/entity_map.rs similarity index 100% rename from crates/gpui/src/app/entity_map.rs rename to src/app/entity_map.rs diff --git a/crates/gpui/src/app/test_context.rs b/src/app/test_context.rs similarity index 100% rename from crates/gpui/src/app/test_context.rs rename to src/app/test_context.rs diff --git a/crates/gpui/src/arena.rs b/src/arena.rs similarity index 100% rename from crates/gpui/src/arena.rs rename to src/arena.rs diff --git a/crates/gpui/src/asset_cache.rs b/src/asset_cache.rs similarity index 100% rename from crates/gpui/src/asset_cache.rs rename to src/asset_cache.rs diff --git a/crates/gpui/src/assets.rs b/src/assets.rs similarity index 100% rename from crates/gpui/src/assets.rs rename to src/assets.rs diff --git a/crates/gpui/src/bounds_tree.rs b/src/bounds_tree.rs similarity index 100% rename from crates/gpui/src/bounds_tree.rs rename to src/bounds_tree.rs diff --git a/crates/gpui/src/color.rs b/src/color.rs similarity index 100% rename from crates/gpui/src/color.rs rename to src/color.rs diff --git a/crates/gpui/src/colors.rs b/src/default_colors.rs similarity index 52% rename from crates/gpui/src/colors.rs rename to src/default_colors.rs index ef11ef57fd..e53ac32b8e 100644 --- a/crates/gpui/src/colors.rs +++ b/src/default_colors.rs @@ -7,12 +7,18 @@ use std::sync::Arc; /// These are used for styling base components, examples and more. #[derive(Clone, Debug)] pub struct Colors { - /// Text color + /// Primary text color pub text: Rgba, + /// Muted/secondary text color + pub text_muted: Rgba, /// Selected text color pub selected_text: Rgba, - /// Background color + /// Background color (root level) pub background: Rgba, + /// Surface color (cards, panels, elevated containers) + pub surface: Rgba, + /// Surface color on hover + pub surface_hover: Rgba, /// Disabled color pub disabled: Rgba, /// Selected color @@ -23,6 +29,24 @@ pub struct Colors { pub separator: Rgba, /// Container color pub container: Rgba, + /// Accent/primary action color (macOS blue) + pub accent: Rgba, + /// Accent color on hover + pub accent_hover: Rgba, + /// Accent color when active/pressed + pub accent_active: Rgba, + /// Success/positive color + pub success: Rgba, + /// Success color on hover + pub success_hover: Rgba, + /// Warning/caution color + pub warning: Rgba, + /// Warning color on hover + pub warning_hover: Rgba, + /// Error/destructive color + pub error: Rgba, + /// Error color on hover + pub error_hover: Rgba, } impl Default for Colors { @@ -40,31 +64,85 @@ impl Colors { } } - /// Returns the default dark colors. + /// Returns the default dark colors pub fn dark() -> Self { Self { + // Text text: rgb(0xffffff), + text_muted: rgb(0x98989d), selected_text: rgb(0xffffff), disabled: rgb(0x565656), - selected: rgb(0x2457ca), - background: rgb(0x222222), - border: rgb(0x000000), - separator: rgb(0xd9d9d9), + + // Backgrounds + background: rgb(0x1e1e1e), + surface: rgb(0x2d2d2d), + surface_hover: rgb(0x3d3d3d), container: rgb(0x262626), + + // Borders + border: rgb(0x3d3d3d), + separator: rgb(0x3d3d3d), + + // Selection + selected: rgb(0x0058d0), + + // Accent (macOS blue) + accent: rgb(0x0a84ff), + accent_hover: rgb(0x409cff), + accent_active: rgb(0x0071e3), + + // Success (green) + success: rgb(0x30d158), + success_hover: rgb(0x28cd52), + + // Warning (yellow/orange) + warning: rgb(0xffd60a), + warning_hover: rgb(0xffcc00), + + // Error (red) + error: rgb(0xff453a), + error_hover: rgb(0xff6961), } } - /// Returns the default light colors. + /// Returns the default light colors pub fn light() -> Self { Self { - text: rgb(0x252525), + // Text + text: rgb(0x1d1d1f), + text_muted: rgb(0x86868b), selected_text: rgb(0xffffff), - background: rgb(0xffffff), disabled: rgb(0xb0b0b0), - selected: rgb(0x2a63d9), - border: rgb(0xd9d9d9), - separator: rgb(0xe6e6e6), - container: rgb(0xf4f5f5), + + // Backgrounds + background: rgb(0xffffff), + surface: rgb(0xf5f5f7), + surface_hover: rgb(0xe8e8ed), + container: rgb(0xf5f5f7), + + // Borders + border: rgb(0xd2d2d7), + separator: rgb(0xd2d2d7), + + // Selection + selected: rgb(0x0066cc), + + // Accent (macOS blue) + accent: rgb(0x007aff), + accent_hover: rgb(0x0071e3), + accent_active: rgb(0x0058d0), + + // Success (green) + success: rgb(0x28cd41), + success_hover: rgb(0x23b839), + + // Warning (yellow/orange) + warning: rgb(0xff9f0a), + warning_hover: rgb(0xe68f09), + + // Error (red) + error: rgb(0xff3b30), + error_hover: rgb(0xe6352b), } } diff --git a/crates/gpui/src/element.rs b/src/element.rs similarity index 100% rename from crates/gpui/src/element.rs rename to src/element.rs diff --git a/crates/gpui/src/elements/anchored.rs b/src/elements/anchored.rs similarity index 100% rename from crates/gpui/src/elements/anchored.rs rename to src/elements/anchored.rs diff --git a/crates/gpui/src/elements/animation.rs b/src/elements/animation.rs similarity index 100% rename from crates/gpui/src/elements/animation.rs rename to src/elements/animation.rs diff --git a/crates/gpui/src/elements/canvas.rs b/src/elements/canvas.rs similarity index 100% rename from crates/gpui/src/elements/canvas.rs rename to src/elements/canvas.rs diff --git a/crates/gpui/src/elements/deferred.rs b/src/elements/deferred.rs similarity index 100% rename from crates/gpui/src/elements/deferred.rs rename to src/elements/deferred.rs diff --git a/crates/gpui/src/elements/div.rs b/src/elements/div.rs similarity index 100% rename from crates/gpui/src/elements/div.rs rename to src/elements/div.rs diff --git a/crates/gpui/src/elements/image_cache.rs b/src/elements/image_cache.rs similarity index 100% rename from crates/gpui/src/elements/image_cache.rs rename to src/elements/image_cache.rs diff --git a/crates/gpui/src/elements/img.rs b/src/elements/img.rs similarity index 100% rename from crates/gpui/src/elements/img.rs rename to src/elements/img.rs diff --git a/crates/gpui/src/elements/list.rs b/src/elements/list.rs similarity index 100% rename from crates/gpui/src/elements/list.rs rename to src/elements/list.rs diff --git a/crates/gpui/src/elements/mod.rs b/src/elements/mod.rs similarity index 100% rename from crates/gpui/src/elements/mod.rs rename to src/elements/mod.rs diff --git a/crates/gpui/src/elements/surface.rs b/src/elements/surface.rs similarity index 100% rename from crates/gpui/src/elements/surface.rs rename to src/elements/surface.rs diff --git a/crates/gpui/src/elements/svg.rs b/src/elements/svg.rs similarity index 100% rename from crates/gpui/src/elements/svg.rs rename to src/elements/svg.rs diff --git a/crates/gpui/src/elements/text.rs b/src/elements/text.rs similarity index 100% rename from crates/gpui/src/elements/text.rs rename to src/elements/text.rs diff --git a/crates/gpui/src/elements/uniform_list.rs b/src/elements/uniform_list.rs similarity index 100% rename from crates/gpui/src/elements/uniform_list.rs rename to src/elements/uniform_list.rs diff --git a/crates/gpui/src/executor.rs b/src/executor.rs similarity index 100% rename from crates/gpui/src/executor.rs rename to src/executor.rs diff --git a/crates/gpui/src/geometry.rs b/src/geometry.rs similarity index 100% rename from crates/gpui/src/geometry.rs rename to src/geometry.rs diff --git a/crates/gpui/src/global.rs b/src/global.rs similarity index 100% rename from crates/gpui/src/global.rs rename to src/global.rs diff --git a/crates/gpui/src/gpui.rs b/src/gpui.rs similarity index 99% rename from crates/gpui/src/gpui.rs rename to src/gpui.rs index e5c726f58e..b2761784ff 100644 --- a/crates/gpui/src/gpui.rs +++ b/src/gpui.rs @@ -16,7 +16,7 @@ mod assets; mod bounds_tree; mod color; /// The default colors used by GPUI. -pub mod colors; +pub mod default_colors; mod element; mod elements; mod executor; @@ -76,6 +76,7 @@ pub use asset_cache::*; pub use assets::*; pub use color::*; pub use ctor::ctor; +pub use default_colors::*; pub use element::*; pub use elements::*; pub use executor::*; diff --git a/crates/gpui/src/input.rs b/src/input.rs similarity index 100% rename from crates/gpui/src/input.rs rename to src/input.rs diff --git a/crates/gpui/src/inspector.rs b/src/inspector.rs similarity index 100% rename from crates/gpui/src/inspector.rs rename to src/inspector.rs diff --git a/crates/gpui/src/interactive.rs b/src/interactive.rs similarity index 100% rename from crates/gpui/src/interactive.rs rename to src/interactive.rs diff --git a/crates/gpui/src/key_dispatch.rs b/src/key_dispatch.rs similarity index 100% rename from crates/gpui/src/key_dispatch.rs rename to src/key_dispatch.rs diff --git a/crates/gpui/src/keymap.rs b/src/keymap.rs similarity index 100% rename from crates/gpui/src/keymap.rs rename to src/keymap.rs diff --git a/crates/gpui/src/keymap/binding.rs b/src/keymap/binding.rs similarity index 100% rename from crates/gpui/src/keymap/binding.rs rename to src/keymap/binding.rs diff --git a/crates/gpui/src/keymap/context.rs b/src/keymap/context.rs similarity index 100% rename from crates/gpui/src/keymap/context.rs rename to src/keymap/context.rs diff --git a/crates/gpui/src/path_builder.rs b/src/path_builder.rs similarity index 100% rename from crates/gpui/src/path_builder.rs rename to src/path_builder.rs diff --git a/crates/gpui/src/platform.rs b/src/platform.rs similarity index 100% rename from crates/gpui/src/platform.rs rename to src/platform.rs diff --git a/crates/gpui/src/platform/app_menu.rs b/src/platform/app_menu.rs similarity index 100% rename from crates/gpui/src/platform/app_menu.rs rename to src/platform/app_menu.rs diff --git a/crates/gpui/src/platform/blade.rs b/src/platform/blade.rs similarity index 100% rename from crates/gpui/src/platform/blade.rs rename to src/platform/blade.rs diff --git a/crates/gpui/src/platform/blade/apple_compat.rs b/src/platform/blade/apple_compat.rs similarity index 100% rename from crates/gpui/src/platform/blade/apple_compat.rs rename to src/platform/blade/apple_compat.rs diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/src/platform/blade/blade_atlas.rs similarity index 100% rename from crates/gpui/src/platform/blade/blade_atlas.rs rename to src/platform/blade/blade_atlas.rs diff --git a/crates/gpui/src/platform/blade/blade_context.rs b/src/platform/blade/blade_context.rs similarity index 100% rename from crates/gpui/src/platform/blade/blade_context.rs rename to src/platform/blade/blade_context.rs diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/src/platform/blade/blade_renderer.rs similarity index 100% rename from crates/gpui/src/platform/blade/blade_renderer.rs rename to src/platform/blade/blade_renderer.rs diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/src/platform/blade/shaders.wgsl similarity index 100% rename from crates/gpui/src/platform/blade/shaders.wgsl rename to src/platform/blade/shaders.wgsl diff --git a/crates/gpui/src/platform/keyboard.rs b/src/platform/keyboard.rs similarity index 100% rename from crates/gpui/src/platform/keyboard.rs rename to src/platform/keyboard.rs diff --git a/crates/gpui/src/platform/keystroke.rs b/src/platform/keystroke.rs similarity index 100% rename from crates/gpui/src/platform/keystroke.rs rename to src/platform/keystroke.rs diff --git a/crates/gpui/src/platform/linux.rs b/src/platform/linux.rs similarity index 100% rename from crates/gpui/src/platform/linux.rs rename to src/platform/linux.rs diff --git a/crates/gpui/src/platform/linux/dispatcher.rs b/src/platform/linux/dispatcher.rs similarity index 100% rename from crates/gpui/src/platform/linux/dispatcher.rs rename to src/platform/linux/dispatcher.rs diff --git a/crates/gpui/src/platform/linux/headless.rs b/src/platform/linux/headless.rs similarity index 100% rename from crates/gpui/src/platform/linux/headless.rs rename to src/platform/linux/headless.rs diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/src/platform/linux/headless/client.rs similarity index 100% rename from crates/gpui/src/platform/linux/headless/client.rs rename to src/platform/linux/headless/client.rs diff --git a/crates/gpui/src/platform/linux/keyboard.rs b/src/platform/linux/keyboard.rs similarity index 100% rename from crates/gpui/src/platform/linux/keyboard.rs rename to src/platform/linux/keyboard.rs diff --git a/crates/gpui/src/platform/linux/platform.rs b/src/platform/linux/platform.rs similarity index 100% rename from crates/gpui/src/platform/linux/platform.rs rename to src/platform/linux/platform.rs diff --git a/crates/gpui/src/platform/linux/text_system.rs b/src/platform/linux/text_system.rs similarity index 100% rename from crates/gpui/src/platform/linux/text_system.rs rename to src/platform/linux/text_system.rs diff --git a/crates/gpui/src/platform/linux/wayland.rs b/src/platform/linux/wayland.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland.rs rename to src/platform/linux/wayland.rs diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/src/platform/linux/wayland/client.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland/client.rs rename to src/platform/linux/wayland/client.rs diff --git a/crates/gpui/src/platform/linux/wayland/clipboard.rs b/src/platform/linux/wayland/clipboard.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland/clipboard.rs rename to src/platform/linux/wayland/clipboard.rs diff --git a/crates/gpui/src/platform/linux/wayland/cursor.rs b/src/platform/linux/wayland/cursor.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland/cursor.rs rename to src/platform/linux/wayland/cursor.rs diff --git a/crates/gpui/src/platform/linux/wayland/display.rs b/src/platform/linux/wayland/display.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland/display.rs rename to src/platform/linux/wayland/display.rs diff --git a/crates/gpui/src/platform/linux/wayland/layer_shell.rs b/src/platform/linux/wayland/layer_shell.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland/layer_shell.rs rename to src/platform/linux/wayland/layer_shell.rs diff --git a/crates/gpui/src/platform/linux/wayland/serial.rs b/src/platform/linux/wayland/serial.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland/serial.rs rename to src/platform/linux/wayland/serial.rs diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/src/platform/linux/wayland/window.rs similarity index 100% rename from crates/gpui/src/platform/linux/wayland/window.rs rename to src/platform/linux/wayland/window.rs diff --git a/crates/gpui/src/platform/linux/x11.rs b/src/platform/linux/x11.rs similarity index 100% rename from crates/gpui/src/platform/linux/x11.rs rename to src/platform/linux/x11.rs diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/src/platform/linux/x11/client.rs similarity index 100% rename from crates/gpui/src/platform/linux/x11/client.rs rename to src/platform/linux/x11/client.rs diff --git a/crates/gpui/src/platform/linux/x11/clipboard.rs b/src/platform/linux/x11/clipboard.rs similarity index 100% rename from crates/gpui/src/platform/linux/x11/clipboard.rs rename to src/platform/linux/x11/clipboard.rs diff --git a/crates/gpui/src/platform/linux/x11/display.rs b/src/platform/linux/x11/display.rs similarity index 100% rename from crates/gpui/src/platform/linux/x11/display.rs rename to src/platform/linux/x11/display.rs diff --git a/crates/gpui/src/platform/linux/x11/event.rs b/src/platform/linux/x11/event.rs similarity index 100% rename from crates/gpui/src/platform/linux/x11/event.rs rename to src/platform/linux/x11/event.rs diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/src/platform/linux/x11/window.rs similarity index 100% rename from crates/gpui/src/platform/linux/x11/window.rs rename to src/platform/linux/x11/window.rs diff --git a/crates/gpui/src/platform/linux/x11/xim_handler.rs b/src/platform/linux/x11/xim_handler.rs similarity index 100% rename from crates/gpui/src/platform/linux/x11/xim_handler.rs rename to src/platform/linux/x11/xim_handler.rs diff --git a/crates/gpui/src/platform/linux/xdg_desktop_portal.rs b/src/platform/linux/xdg_desktop_portal.rs similarity index 100% rename from crates/gpui/src/platform/linux/xdg_desktop_portal.rs rename to src/platform/linux/xdg_desktop_portal.rs diff --git a/crates/gpui/src/platform/mac.rs b/src/platform/mac.rs similarity index 100% rename from crates/gpui/src/platform/mac.rs rename to src/platform/mac.rs diff --git a/crates/gpui/src/platform/mac/attributed_string.rs b/src/platform/mac/attributed_string.rs similarity index 100% rename from crates/gpui/src/platform/mac/attributed_string.rs rename to src/platform/mac/attributed_string.rs diff --git a/crates/gpui/src/platform/mac/dispatch.h b/src/platform/mac/dispatch.h similarity index 100% rename from crates/gpui/src/platform/mac/dispatch.h rename to src/platform/mac/dispatch.h diff --git a/crates/gpui/src/platform/mac/dispatcher.rs b/src/platform/mac/dispatcher.rs similarity index 100% rename from crates/gpui/src/platform/mac/dispatcher.rs rename to src/platform/mac/dispatcher.rs diff --git a/crates/gpui/src/platform/mac/display.rs b/src/platform/mac/display.rs similarity index 100% rename from crates/gpui/src/platform/mac/display.rs rename to src/platform/mac/display.rs diff --git a/crates/gpui/src/platform/mac/display_link.rs b/src/platform/mac/display_link.rs similarity index 100% rename from crates/gpui/src/platform/mac/display_link.rs rename to src/platform/mac/display_link.rs diff --git a/crates/gpui/src/platform/mac/events.rs b/src/platform/mac/events.rs similarity index 100% rename from crates/gpui/src/platform/mac/events.rs rename to src/platform/mac/events.rs diff --git a/crates/gpui/src/platform/mac/keyboard.rs b/src/platform/mac/keyboard.rs similarity index 100% rename from crates/gpui/src/platform/mac/keyboard.rs rename to src/platform/mac/keyboard.rs diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/src/platform/mac/metal_atlas.rs similarity index 100% rename from crates/gpui/src/platform/mac/metal_atlas.rs rename to src/platform/mac/metal_atlas.rs diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/src/platform/mac/metal_renderer.rs similarity index 100% rename from crates/gpui/src/platform/mac/metal_renderer.rs rename to src/platform/mac/metal_renderer.rs diff --git a/crates/gpui/src/platform/mac/open_type.rs b/src/platform/mac/open_type.rs similarity index 100% rename from crates/gpui/src/platform/mac/open_type.rs rename to src/platform/mac/open_type.rs diff --git a/crates/gpui/src/platform/mac/platform.rs b/src/platform/mac/platform.rs similarity index 100% rename from crates/gpui/src/platform/mac/platform.rs rename to src/platform/mac/platform.rs diff --git a/crates/gpui/src/platform/mac/screen_capture.rs b/src/platform/mac/screen_capture.rs similarity index 100% rename from crates/gpui/src/platform/mac/screen_capture.rs rename to src/platform/mac/screen_capture.rs diff --git a/crates/gpui/src/platform/mac/shaders.metal b/src/platform/mac/shaders.metal similarity index 100% rename from crates/gpui/src/platform/mac/shaders.metal rename to src/platform/mac/shaders.metal diff --git a/crates/gpui/src/platform/mac/status_item.rs b/src/platform/mac/status_item.rs similarity index 100% rename from crates/gpui/src/platform/mac/status_item.rs rename to src/platform/mac/status_item.rs diff --git a/crates/gpui/src/platform/mac/text_system.rs b/src/platform/mac/text_system.rs similarity index 100% rename from crates/gpui/src/platform/mac/text_system.rs rename to src/platform/mac/text_system.rs diff --git a/crates/gpui/src/platform/mac/window.rs b/src/platform/mac/window.rs similarity index 100% rename from crates/gpui/src/platform/mac/window.rs rename to src/platform/mac/window.rs diff --git a/crates/gpui/src/platform/mac/window_appearance.rs b/src/platform/mac/window_appearance.rs similarity index 100% rename from crates/gpui/src/platform/mac/window_appearance.rs rename to src/platform/mac/window_appearance.rs diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/src/platform/scap_screen_capture.rs similarity index 100% rename from crates/gpui/src/platform/scap_screen_capture.rs rename to src/platform/scap_screen_capture.rs diff --git a/crates/gpui/src/platform/test.rs b/src/platform/test.rs similarity index 100% rename from crates/gpui/src/platform/test.rs rename to src/platform/test.rs diff --git a/crates/gpui/src/platform/test/dispatcher.rs b/src/platform/test/dispatcher.rs similarity index 100% rename from crates/gpui/src/platform/test/dispatcher.rs rename to src/platform/test/dispatcher.rs diff --git a/crates/gpui/src/platform/test/display.rs b/src/platform/test/display.rs similarity index 100% rename from crates/gpui/src/platform/test/display.rs rename to src/platform/test/display.rs diff --git a/crates/gpui/src/platform/test/platform.rs b/src/platform/test/platform.rs similarity index 100% rename from crates/gpui/src/platform/test/platform.rs rename to src/platform/test/platform.rs diff --git a/crates/gpui/src/platform/test/window.rs b/src/platform/test/window.rs similarity index 100% rename from crates/gpui/src/platform/test/window.rs rename to src/platform/test/window.rs diff --git a/crates/gpui/src/platform/windows.rs b/src/platform/windows.rs similarity index 100% rename from crates/gpui/src/platform/windows.rs rename to src/platform/windows.rs diff --git a/crates/gpui/src/platform/windows/alpha_correction.hlsl b/src/platform/windows/alpha_correction.hlsl similarity index 100% rename from crates/gpui/src/platform/windows/alpha_correction.hlsl rename to src/platform/windows/alpha_correction.hlsl diff --git a/crates/gpui/src/platform/windows/clipboard.rs b/src/platform/windows/clipboard.rs similarity index 100% rename from crates/gpui/src/platform/windows/clipboard.rs rename to src/platform/windows/clipboard.rs diff --git a/crates/gpui/src/platform/windows/color_text_raster.hlsl b/src/platform/windows/color_text_raster.hlsl similarity index 100% rename from crates/gpui/src/platform/windows/color_text_raster.hlsl rename to src/platform/windows/color_text_raster.hlsl diff --git a/crates/gpui/src/platform/windows/destination_list.rs b/src/platform/windows/destination_list.rs similarity index 100% rename from crates/gpui/src/platform/windows/destination_list.rs rename to src/platform/windows/destination_list.rs diff --git a/crates/gpui/src/platform/windows/direct_write.rs b/src/platform/windows/direct_write.rs similarity index 100% rename from crates/gpui/src/platform/windows/direct_write.rs rename to src/platform/windows/direct_write.rs diff --git a/crates/gpui/src/platform/windows/directx_atlas.rs b/src/platform/windows/directx_atlas.rs similarity index 100% rename from crates/gpui/src/platform/windows/directx_atlas.rs rename to src/platform/windows/directx_atlas.rs diff --git a/crates/gpui/src/platform/windows/directx_devices.rs b/src/platform/windows/directx_devices.rs similarity index 100% rename from crates/gpui/src/platform/windows/directx_devices.rs rename to src/platform/windows/directx_devices.rs diff --git a/crates/gpui/src/platform/windows/directx_renderer.rs b/src/platform/windows/directx_renderer.rs similarity index 100% rename from crates/gpui/src/platform/windows/directx_renderer.rs rename to src/platform/windows/directx_renderer.rs diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/src/platform/windows/dispatcher.rs similarity index 100% rename from crates/gpui/src/platform/windows/dispatcher.rs rename to src/platform/windows/dispatcher.rs diff --git a/crates/gpui/src/platform/windows/display.rs b/src/platform/windows/display.rs similarity index 100% rename from crates/gpui/src/platform/windows/display.rs rename to src/platform/windows/display.rs diff --git a/crates/gpui/src/platform/windows/events.rs b/src/platform/windows/events.rs similarity index 100% rename from crates/gpui/src/platform/windows/events.rs rename to src/platform/windows/events.rs diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/src/platform/windows/keyboard.rs similarity index 100% rename from crates/gpui/src/platform/windows/keyboard.rs rename to src/platform/windows/keyboard.rs diff --git a/crates/gpui/src/platform/windows/platform.rs b/src/platform/windows/platform.rs similarity index 100% rename from crates/gpui/src/platform/windows/platform.rs rename to src/platform/windows/platform.rs diff --git a/crates/gpui/src/platform/windows/shaders.hlsl b/src/platform/windows/shaders.hlsl similarity index 100% rename from crates/gpui/src/platform/windows/shaders.hlsl rename to src/platform/windows/shaders.hlsl diff --git a/crates/gpui/src/platform/windows/system_settings.rs b/src/platform/windows/system_settings.rs similarity index 100% rename from crates/gpui/src/platform/windows/system_settings.rs rename to src/platform/windows/system_settings.rs diff --git a/crates/gpui/src/platform/windows/util.rs b/src/platform/windows/util.rs similarity index 100% rename from crates/gpui/src/platform/windows/util.rs rename to src/platform/windows/util.rs diff --git a/crates/gpui/src/platform/windows/vsync.rs b/src/platform/windows/vsync.rs similarity index 100% rename from crates/gpui/src/platform/windows/vsync.rs rename to src/platform/windows/vsync.rs diff --git a/crates/gpui/src/platform/windows/window.rs b/src/platform/windows/window.rs similarity index 100% rename from crates/gpui/src/platform/windows/window.rs rename to src/platform/windows/window.rs diff --git a/crates/gpui/src/platform/windows/wrapper.rs b/src/platform/windows/wrapper.rs similarity index 100% rename from crates/gpui/src/platform/windows/wrapper.rs rename to src/platform/windows/wrapper.rs diff --git a/crates/gpui/src/prelude.rs b/src/prelude.rs similarity index 100% rename from crates/gpui/src/prelude.rs rename to src/prelude.rs diff --git a/crates/gpui/src/profiler.rs b/src/profiler.rs similarity index 100% rename from crates/gpui/src/profiler.rs rename to src/profiler.rs diff --git a/crates/gpui/src/queue.rs b/src/queue.rs similarity index 100% rename from crates/gpui/src/queue.rs rename to src/queue.rs diff --git a/crates/gpui/src/scene.rs b/src/scene.rs similarity index 100% rename from crates/gpui/src/scene.rs rename to src/scene.rs diff --git a/crates/gpui/src/shared_string.rs b/src/shared_string.rs similarity index 100% rename from crates/gpui/src/shared_string.rs rename to src/shared_string.rs diff --git a/crates/gpui/src/shared_uri.rs b/src/shared_uri.rs similarity index 100% rename from crates/gpui/src/shared_uri.rs rename to src/shared_uri.rs diff --git a/crates/gpui/src/style.rs b/src/style.rs similarity index 100% rename from crates/gpui/src/style.rs rename to src/style.rs diff --git a/crates/gpui/src/styled.rs b/src/styled.rs similarity index 100% rename from crates/gpui/src/styled.rs rename to src/styled.rs diff --git a/crates/gpui/src/subscription.rs b/src/subscription.rs similarity index 100% rename from crates/gpui/src/subscription.rs rename to src/subscription.rs diff --git a/crates/gpui/src/svg_renderer.rs b/src/svg_renderer.rs similarity index 100% rename from crates/gpui/src/svg_renderer.rs rename to src/svg_renderer.rs diff --git a/crates/gpui/src/tab_stop.rs b/src/tab_stop.rs similarity index 100% rename from crates/gpui/src/tab_stop.rs rename to src/tab_stop.rs diff --git a/crates/gpui/src/taffy.rs b/src/taffy.rs similarity index 100% rename from crates/gpui/src/taffy.rs rename to src/taffy.rs diff --git a/crates/gpui/src/test.rs b/src/test.rs similarity index 100% rename from crates/gpui/src/test.rs rename to src/test.rs diff --git a/crates/gpui/src/text_system.rs b/src/text_system.rs similarity index 100% rename from crates/gpui/src/text_system.rs rename to src/text_system.rs diff --git a/crates/gpui/src/text_system/font_fallbacks.rs b/src/text_system/font_fallbacks.rs similarity index 100% rename from crates/gpui/src/text_system/font_fallbacks.rs rename to src/text_system/font_fallbacks.rs diff --git a/crates/gpui/src/text_system/font_features.rs b/src/text_system/font_features.rs similarity index 100% rename from crates/gpui/src/text_system/font_features.rs rename to src/text_system/font_features.rs diff --git a/crates/gpui/src/text_system/line.rs b/src/text_system/line.rs similarity index 100% rename from crates/gpui/src/text_system/line.rs rename to src/text_system/line.rs diff --git a/crates/gpui/src/text_system/line_layout.rs b/src/text_system/line_layout.rs similarity index 100% rename from crates/gpui/src/text_system/line_layout.rs rename to src/text_system/line_layout.rs diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/src/text_system/line_wrapper.rs similarity index 100% rename from crates/gpui/src/text_system/line_wrapper.rs rename to src/text_system/line_wrapper.rs diff --git a/crates/gpui/src/util.rs b/src/util.rs similarity index 100% rename from crates/gpui/src/util.rs rename to src/util.rs diff --git a/crates/gpui/src/view.rs b/src/view.rs similarity index 100% rename from crates/gpui/src/view.rs rename to src/view.rs diff --git a/crates/gpui/src/window.rs b/src/window.rs similarity index 100% rename from crates/gpui/src/window.rs rename to src/window.rs diff --git a/crates/gpui/src/window/prompts.rs b/src/window/prompts.rs similarity index 100% rename from crates/gpui/src/window/prompts.rs rename to src/window/prompts.rs diff --git a/crates/gpui/tests/action_macros.rs b/tests/action_macros.rs similarity index 100% rename from crates/gpui/tests/action_macros.rs rename to tests/action_macros.rs diff --git a/tooling/perf/Cargo.toml b/tooling/perf/Cargo.toml deleted file mode 100644 index c9018de010..0000000000 --- a/tooling/perf/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "perf" -version = "0.1.0" -publish = false -edition.workspace = true -license = "Apache-2.0" -description = "A tool for measuring GPUI test performance" - -[lib] - -# Some personal lint preferences :3 -[lints.rust] -missing_docs = "warn" - -[lints.clippy] -needless_continue = "allow" # For a convenience macro -all = "warn" -pedantic = "warn" -style = "warn" -missing_docs_in_private_items = "warn" -as_underscore = "deny" -allow_attributes = "deny" -allow_attributes_without_reason = "deny" # This covers `expect` also, since we deny `allow` -let_underscore_must_use = "forbid" -undocumented_unsafe_blocks = "forbid" -missing_safety_doc = "forbid" -disallowed_methods = { level = "allow", priority = 1} - -[dependencies] -collections.workspace = true -serde.workspace = true -serde_json.workspace = true diff --git a/tooling/perf/LICENSE-APACHE b/tooling/perf/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/tooling/perf/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/tooling/perf/src/implementation.rs b/tooling/perf/src/implementation.rs deleted file mode 100644 index c151dda91f..0000000000 --- a/tooling/perf/src/implementation.rs +++ /dev/null @@ -1,450 +0,0 @@ -//! The implementation of the this crate is kept in a separate module -//! so that it is easy to publish this crate as part of GPUI's dependencies - -use collections::HashMap; -use serde::{Deserialize, Serialize}; -use std::{num::NonZero, time::Duration}; - -pub mod consts { - //! Preset identifiers and constants so that the profiler and proc macro agree - //! on their communication protocol. - - /// The suffix on the actual test function. - pub const SUF_NORMAL: &str = "__ZED_PERF_FN"; - /// The suffix on an extra function which prints metadata about a test to stdout. - pub const SUF_MDATA: &str = "__ZED_PERF_MDATA"; - /// The env var in which we pass the iteration count to our tests. - pub const ITER_ENV_VAR: &str = "ZED_PERF_ITER"; - /// The prefix printed on all benchmark test metadata lines, to distinguish it from - /// possible output by the test harness itself. - pub const MDATA_LINE_PREF: &str = "ZED_MDATA_"; - /// The version number for the data returned from the test metadata function. - /// Increment on non-backwards-compatible changes. - pub const MDATA_VER: u32 = 0; - /// The default weight, if none is specified. - pub const WEIGHT_DEFAULT: u8 = 50; - /// How long a test must have run to be assumed to be reliable-ish. - pub const NOISE_CUTOFF: std::time::Duration = std::time::Duration::from_millis(250); - - /// Identifier for the iteration count of a test metadata. - pub const ITER_COUNT_LINE_NAME: &str = "iter_count"; - /// Identifier for the weight of a test metadata. - pub const WEIGHT_LINE_NAME: &str = "weight"; - /// Identifier for importance in test metadata. - pub const IMPORTANCE_LINE_NAME: &str = "importance"; - /// Identifier for the test metadata version. - pub const VERSION_LINE_NAME: &str = "version"; - - /// Where to save json run information. - pub const RUNS_DIR: &str = ".perf-runs"; -} - -/// How relevant a benchmark is. -#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] -pub enum Importance { - /// Regressions shouldn't be accepted without good reason. - Critical = 4, - /// Regressions should be paid extra attention. - Important = 3, - /// No extra attention should be paid to regressions, but they might still - /// be indicative of something happening. - #[default] - Average = 2, - /// Unclear if regressions are likely to be meaningful, but still worth keeping - /// an eye on. Lowest level that's checked by default by the profiler. - Iffy = 1, - /// Regressions are likely to be spurious or don't affect core functionality. - /// Only relevant if a lot of them happen, or as supplemental evidence for a - /// higher-importance benchmark regressing. Not checked by default. - Fluff = 0, -} - -impl std::fmt::Display for Importance { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Importance::Critical => f.write_str("critical"), - Importance::Important => f.write_str("important"), - Importance::Average => f.write_str("average"), - Importance::Iffy => f.write_str("iffy"), - Importance::Fluff => f.write_str("fluff"), - } - } -} - -/// Why or when did this test fail? -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum FailKind { - /// Failed while triaging it to determine the iteration count. - Triage, - /// Failed while profiling it. - Profile, - /// Failed due to an incompatible version for the test. - VersionMismatch, - /// Could not parse metadata for a test. - BadMetadata, - /// Skipped due to filters applied on the perf run. - Skipped, -} - -impl std::fmt::Display for FailKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - FailKind::Triage => f.write_str("errored in triage"), - FailKind::Profile => f.write_str("errored while profiling"), - FailKind::VersionMismatch => f.write_str("test version mismatch"), - FailKind::BadMetadata => f.write_str("bad test metadata"), - FailKind::Skipped => f.write_str("skipped"), - } - } -} - -/// Information about a given perf test. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct TestMdata { - /// A version number for when the test was generated. If this is greater - /// than the version this test handler expects, one of the following will - /// happen in an unspecified manner: - /// - The test is skipped silently. - /// - The handler exits with an error message indicating the version mismatch - /// or inability to parse the metadata. - /// - /// INVARIANT: If `version` <= `MDATA_VER`, this tool *must* be able to - /// correctly parse the output of this test. - pub version: u32, - /// How many iterations to pass this test if this is preset, or how many - /// iterations a test ended up running afterwards if determined at runtime. - pub iterations: Option>, - /// The importance of this particular test. See the docs on `Importance` for - /// details. - pub importance: Importance, - /// The weight of this particular test within its importance category. Used - /// when comparing across runs. - pub weight: u8, -} - -/// The actual timings of a test, as measured by Hyperfine. -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Timings { - /// Mean runtime for `self.iter_total` runs of this test. - pub mean: Duration, - /// Standard deviation for the above. - pub stddev: Duration, -} - -impl Timings { - /// How many iterations does this test seem to do per second? - #[expect( - clippy::cast_precision_loss, - reason = "We only care about a couple sig figs anyways" - )] - #[must_use] - pub fn iters_per_sec(&self, total_iters: NonZero) -> f64 { - (1000. / self.mean.as_millis() as f64) * total_iters.get() as f64 - } -} - -/// Aggregate results, meant to be used for a given importance category. Each -/// test name corresponds to its benchmark results, iteration count, and weight. -type CategoryInfo = HashMap, u8)>; - -/// Aggregate output of all tests run by this handler. -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct Output { - /// A list of test outputs. Format is `(test_name, mdata, timings)`. - /// The latter being `Ok(_)` indicates the test succeeded. - /// - /// INVARIANT: If the test succeeded, the second field is `Some(mdata)` and - /// `mdata.iterations` is `Some(_)`. - tests: Vec<(String, Option, Result)>, -} - -impl Output { - /// Instantiates an empty "output". Useful for merging. - #[must_use] - pub fn blank() -> Self { - Output { tests: Vec::new() } - } - - /// Reports a success and adds it to this run's `Output`. - pub fn success( - &mut self, - name: impl AsRef, - mut mdata: TestMdata, - iters: NonZero, - timings: Timings, - ) { - mdata.iterations = Some(iters); - self.tests - .push((name.as_ref().to_string(), Some(mdata), Ok(timings))); - } - - /// Reports a failure and adds it to this run's `Output`. If this test was tried - /// with some number of iterations (i.e. this was not a version mismatch or skipped - /// test), it should be reported also. - /// - /// Using the `fail!()` macro is usually more convenient. - pub fn failure( - &mut self, - name: impl AsRef, - mut mdata: Option, - attempted_iters: Option>, - kind: FailKind, - ) { - if let Some(ref mut mdata) = mdata { - mdata.iterations = attempted_iters; - } - self.tests - .push((name.as_ref().to_string(), mdata, Err(kind))); - } - - /// True if no tests executed this run. - #[must_use] - pub fn is_empty(&self) -> bool { - self.tests.is_empty() - } - - /// Sorts the runs in the output in the order that we want them printed. - pub fn sort(&mut self) { - self.tests.sort_unstable_by(|a, b| match (a, b) { - // Tests where we got no metadata go at the end. - ((_, Some(_), _), (_, None, _)) => std::cmp::Ordering::Greater, - ((_, None, _), (_, Some(_), _)) => std::cmp::Ordering::Less, - // Then sort by importance, then weight. - ((_, Some(a_mdata), _), (_, Some(b_mdata), _)) => { - let c = a_mdata.importance.cmp(&b_mdata.importance); - if matches!(c, std::cmp::Ordering::Equal) { - a_mdata.weight.cmp(&b_mdata.weight) - } else { - c - } - } - // Lastly by name. - ((a_name, ..), (b_name, ..)) => a_name.cmp(b_name), - }); - } - - /// Merges the output of two runs, appending a prefix to the results of the new run. - /// To be used in conjunction with `Output::blank()`, or else only some tests will have - /// a prefix set. - pub fn merge<'a>(&mut self, other: Self, pref_other: impl Into>) { - let pref = if let Some(pref) = pref_other.into() { - "crates/".to_string() + pref + "::" - } else { - String::new() - }; - self.tests = std::mem::take(&mut self.tests) - .into_iter() - .chain( - other - .tests - .into_iter() - .map(|(name, md, tm)| (pref.clone() + &name, md, tm)), - ) - .collect(); - } - - /// Evaluates the performance of `self` against `baseline`. The latter is taken - /// as the comparison point, i.e. a positive resulting `PerfReport` means that - /// `self` performed better. - /// - /// # Panics - /// `self` and `baseline` are assumed to have the iterations field on all - /// `TestMdata`s set to `Some(_)` if the `TestMdata` is present itself. - #[must_use] - pub fn compare_perf(self, baseline: Self) -> PerfReport { - let self_categories = self.collapse(); - let mut other_categories = baseline.collapse(); - - let deltas = self_categories - .into_iter() - .filter_map(|(cat, self_data)| { - // Only compare categories where both meow - // runs have data. / - let mut other_data = other_categories.remove(&cat)?; - let mut max = f64::MIN; - let mut min = f64::MAX; - - // Running totals for averaging out tests. - let mut r_total_numerator = 0.; - let mut r_total_denominator = 0; - // Yeah this is O(n^2), but realistically it'll hardly be a bottleneck. - for (name, (s_timings, s_iters, weight)) in self_data { - // Only use the new weights if they conflict. - let Some((o_timings, o_iters, _)) = other_data.remove(&name) else { - continue; - }; - let shift = - (o_timings.iters_per_sec(o_iters) / s_timings.iters_per_sec(s_iters)) - 1.; - if shift > max { - max = shift; - } - if shift < min { - min = shift; - } - r_total_numerator += shift * f64::from(weight); - r_total_denominator += u32::from(weight); - } - // There were no runs here! - if r_total_denominator == 0 { - None - } else { - let mean = r_total_numerator / f64::from(r_total_denominator); - // TODO: also aggregate standard deviation? That's harder to keep - // meaningful, though, since we dk which tests are correlated. - Some((cat, PerfDelta { max, mean, min })) - } - }) - .collect(); - - PerfReport { deltas } - } - - /// Collapses the `PerfReport` into a `HashMap` over `Importance`, with - /// each importance category having its tests contained. - fn collapse(self) -> HashMap { - let mut categories = HashMap::>::default(); - for entry in self.tests { - if let Some(mdata) = entry.1 - && let Ok(timings) = entry.2 - { - if let Some(handle) = categories.get_mut(&mdata.importance) { - handle.insert(entry.0, (timings, mdata.iterations.unwrap(), mdata.weight)); - } else { - let mut new = HashMap::default(); - new.insert(entry.0, (timings, mdata.iterations.unwrap(), mdata.weight)); - categories.insert(mdata.importance, new); - } - } - } - - categories - } -} - -impl std::fmt::Display for Output { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Don't print the header for an empty run. - if self.tests.is_empty() { - return Ok(()); - } - - // We want to print important tests at the top, then alphabetical. - let mut sorted = self.clone(); - sorted.sort(); - // Markdown header for making a nice little table :> - writeln!( - f, - "| Command | Iter/sec | Mean [ms] | SD [ms] | Iterations | Importance (weight) |", - )?; - writeln!(f, "|:---|---:|---:|---:|---:|---:|")?; - for (name, metadata, timings) in &sorted.tests { - match metadata { - Some(metadata) => match timings { - // Happy path. - Ok(timings) => { - // If the test succeeded, then metadata.iterations is Some(_). - writeln!( - f, - "| {} | {:.2} | {} | {:.2} | {} | {} ({}) |", - name, - timings.iters_per_sec(metadata.iterations.unwrap()), - { - // Very small mean runtimes will give inaccurate - // results. Should probably also penalise weight. - let mean = timings.mean.as_secs_f64() * 1000.; - if mean < consts::NOISE_CUTOFF.as_secs_f64() * 1000. / 8. { - format!("{mean:.2} (unreliable)") - } else { - format!("{mean:.2}") - } - }, - timings.stddev.as_secs_f64() * 1000., - metadata.iterations.unwrap(), - metadata.importance, - metadata.weight, - )?; - } - // We have (some) metadata, but the test errored. - Err(err) => writeln!( - f, - "| ({}) {} | N/A | N/A | N/A | {} | {} ({}) |", - err, - name, - metadata - .iterations - .map_or_else(|| "N/A".to_owned(), |i| format!("{i}")), - metadata.importance, - metadata.weight - )?, - }, - // No metadata, couldn't even parse the test output. - None => writeln!( - f, - "| ({}) {} | N/A | N/A | N/A | N/A | N/A |", - timings.as_ref().unwrap_err(), - name - )?, - } - } - Ok(()) - } -} - -/// The difference in performance between two runs within a given importance -/// category. -struct PerfDelta { - /// The biggest improvement / least bad regression. - max: f64, - /// The weighted average change in test times. - mean: f64, - /// The worst regression / smallest improvement. - min: f64, -} - -/// Shim type for reporting all performance deltas across importance categories. -pub struct PerfReport { - /// Inner (group, diff) pairing. - deltas: HashMap, -} - -impl std::fmt::Display for PerfReport { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.deltas.is_empty() { - return write!(f, "(no matching tests)"); - } - let sorted = self.deltas.iter().collect::>(); - writeln!(f, "| Category | Max | Mean | Min |")?; - // We don't want to print too many newlines at the end, so handle newlines - // a little jankily like this. - write!(f, "|:---|---:|---:|---:|")?; - for (cat, delta) in sorted.into_iter().rev() { - const SIGN_POS: &str = "↑"; - const SIGN_NEG: &str = "↓"; - const SIGN_NEUTRAL_POS: &str = "±↑"; - const SIGN_NEUTRAL_NEG: &str = "±↓"; - - let prettify = |time: f64| { - let sign = if time > 0.05 { - SIGN_POS - } else if time > 0. { - SIGN_NEUTRAL_POS - } else if time > -0.05 { - SIGN_NEUTRAL_NEG - } else { - SIGN_NEG - }; - format!("{} {:.1}%", sign, time.abs() * 100.) - }; - - // Pretty-print these instead of just using the float display impl. - write!( - f, - "\n| {cat} | {} | {} | {} |", - prettify(delta.max), - prettify(delta.mean), - prettify(delta.min) - )?; - } - Ok(()) - } -} diff --git a/tooling/perf/src/lib.rs b/tooling/perf/src/lib.rs deleted file mode 100644 index 7933e66e79..0000000000 --- a/tooling/perf/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Some constants and datatypes used in the Zed perf profiler. Should only be -//! consumed by the crate providing the matching macros. -//! -//! For usage documentation, see the docs on this crate's binary. - -mod implementation; -pub use implementation::*; diff --git a/tooling/perf/src/main.rs b/tooling/perf/src/main.rs deleted file mode 100644 index 243658e508..0000000000 --- a/tooling/perf/src/main.rs +++ /dev/null @@ -1,582 +0,0 @@ -//! Perf profiler for Zed tests. Outputs timings of tests marked with the `#[perf]` -//! attribute to stdout in Markdown. See the documentation of `util_macros::perf` -//! for usage details on the actual attribute. -//! -//! # Setup -//! Make sure `hyperfine` is installed and in the shell path. -//! -//! # Usage -//! Calling this tool rebuilds the targeted crate(s) with some cfg flags set for the -//! perf proc macro *and* enables optimisations (`release-fast` profile), so expect -//! it to take a little while. -//! -//! To test an individual crate, run: -//! ```sh -//! cargo perf-test -p $CRATE -//! ``` -//! -//! To test everything (which will be **VERY SLOW**), run: -//! ```sh -//! cargo perf-test --workspace -//! ``` -//! -//! Some command-line parameters are also recognised by this profiler. To filter -//! out all tests below a certain importance (e.g. `important`), run: -//! ```sh -//! cargo perf-test $WHATEVER -- --important -//! ``` -//! -//! Similarly, to skip outputting progress to the command line, pass `-- --quiet`. -//! These flags can be combined. -//! -//! ## Comparing runs -//! Passing `--json=ident` will save per-crate run files in `.perf-runs`, e.g. -//! `cargo perf-test -p gpui -- --json=blah` will result in `.perf-runs/blah.gpui.json` -//! being created (unless no tests were run). These results can be automatically -//! compared. To do so, run `cargo perf-compare new-ident old-ident`. -//! -//! To save the markdown output to a file instead, run `cargo perf-compare --save=$FILE -//! new-ident old-ident`. -//! -//! NB: All files matching `.perf-runs/ident.*.json` will be considered when -//! doing this comparison, so ensure there aren't leftover files in your `.perf-runs` -//! directory that might match that! -//! -//! # Notes -//! This should probably not be called manually unless you're working on the profiler -//! itself; use the `cargo perf-test` alias (after building this crate) instead. - -mod implementation; - -use implementation::{FailKind, Importance, Output, TestMdata, Timings, consts}; - -use std::{ - fs::OpenOptions, - io::{Read, Write}, - num::NonZero, - path::{Path, PathBuf}, - process::{Command, Stdio}, - sync::atomic::{AtomicBool, Ordering}, - time::{Duration, Instant}, -}; - -/// How many iterations to attempt the first time a test is run. -const DEFAULT_ITER_COUNT: NonZero = NonZero::new(3).unwrap(); -/// Multiplier for the iteration count when a test doesn't pass the noise cutoff. -const ITER_COUNT_MUL: NonZero = NonZero::new(4).unwrap(); - -/// Do we keep stderr empty while running the tests? -static QUIET: AtomicBool = AtomicBool::new(false); - -/// Report a failure into the output and skip an iteration. -macro_rules! fail { - ($output:ident, $name:expr, $kind:expr) => {{ - $output.failure($name, None, None, $kind); - continue; - }}; - ($output:ident, $name:expr, $mdata:expr, $kind:expr) => {{ - $output.failure($name, Some($mdata), None, $kind); - continue; - }}; - ($output:ident, $name:expr, $mdata:expr, $count:expr, $kind:expr) => {{ - $output.failure($name, Some($mdata), Some($count), $kind); - continue; - }}; -} - -/// How does this perf run return its output? -enum OutputKind<'a> { - /// Print markdown to the terminal. - Markdown, - /// Save JSON to a file. - Json(&'a Path), -} - -impl OutputKind<'_> { - /// Logs the output of a run as per the `OutputKind`. - fn log(&self, output: &Output, t_bin: &str) { - match self { - OutputKind::Markdown => println!("{output}"), - OutputKind::Json(ident) => { - // We're going to be in tooling/perf/$whatever. - let wspace_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()) - .join("..") - .join(".."); - let runs_dir = PathBuf::from(&wspace_dir).join(consts::RUNS_DIR); - std::fs::create_dir_all(&runs_dir).unwrap(); - assert!( - !ident.to_string_lossy().is_empty(), - "FATAL: Empty filename specified!" - ); - // Get the test binary's crate's name; a path like - // target/release-fast/deps/gpui-061ff76c9b7af5d7 - // would be reduced to just "gpui". - let test_bin_stripped = Path::new(t_bin) - .file_name() - .unwrap() - .to_str() - .unwrap() - .rsplit_once('-') - .unwrap() - .0; - let mut file_path = runs_dir.join(ident); - file_path - .as_mut_os_string() - .push(format!(".{test_bin_stripped}.json")); - let mut out_file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&file_path) - .unwrap(); - out_file - .write_all(&serde_json::to_vec(&output).unwrap()) - .unwrap(); - if !QUIET.load(Ordering::Relaxed) { - eprintln!("JSON output written to {}", file_path.display()); - } - } - } - } -} - -/// Runs a given metadata-returning function from a test handler, parsing its -/// output into a `TestMdata`. -fn parse_mdata(t_bin: &str, mdata_fn: &str) -> Result { - let mut cmd = Command::new(t_bin); - cmd.args([mdata_fn, "--exact", "--nocapture"]); - let out = cmd - .output() - .expect("FATAL: Could not run test binary {t_bin}"); - assert!(out.status.success()); - let stdout = String::from_utf8_lossy(&out.stdout); - let mut version = None; - let mut iterations = None; - let mut importance = Importance::default(); - let mut weight = consts::WEIGHT_DEFAULT; - for line in stdout - .lines() - .filter_map(|l| l.strip_prefix(consts::MDATA_LINE_PREF)) - { - let mut items = line.split_whitespace(); - // For v0, we know the ident always comes first, then one field. - match items.next().ok_or(FailKind::BadMetadata)? { - consts::VERSION_LINE_NAME => { - let v = items - .next() - .ok_or(FailKind::BadMetadata)? - .parse::() - .map_err(|_| FailKind::BadMetadata)?; - if v > consts::MDATA_VER { - return Err(FailKind::VersionMismatch); - } - version = Some(v); - } - consts::ITER_COUNT_LINE_NAME => { - // This should never be zero! - iterations = Some( - items - .next() - .ok_or(FailKind::BadMetadata)? - .parse::() - .map_err(|_| FailKind::BadMetadata)? - .try_into() - .map_err(|_| FailKind::BadMetadata)?, - ); - } - consts::IMPORTANCE_LINE_NAME => { - importance = match items.next().ok_or(FailKind::BadMetadata)? { - "critical" => Importance::Critical, - "important" => Importance::Important, - "average" => Importance::Average, - "iffy" => Importance::Iffy, - "fluff" => Importance::Fluff, - _ => return Err(FailKind::BadMetadata), - }; - } - consts::WEIGHT_LINE_NAME => { - weight = items - .next() - .ok_or(FailKind::BadMetadata)? - .parse::() - .map_err(|_| FailKind::BadMetadata)?; - } - _ => unreachable!(), - } - } - - Ok(TestMdata { - version: version.ok_or(FailKind::BadMetadata)?, - // Iterations may be determined by us and thus left unspecified. - iterations, - // In principle this should always be set, but just for the sake of - // stability allow the potentially-breaking change of not reporting the - // importance without erroring. Maybe we want to change this. - importance, - // Same with weight. - weight, - }) -} - -/// Compares the perf results of two profiles as per the arguments passed in. -fn compare_profiles(args: &[String]) { - let mut save_to = None; - let mut ident_idx = 0; - args.first().inspect(|a| { - if a.starts_with("--save") { - save_to = Some( - a.strip_prefix("--save=") - .expect("FATAL: save param formatted incorrectly"), - ); - ident_idx = 1; - } - }); - let ident_new = args - .get(ident_idx) - .expect("FATAL: missing identifier for new run"); - let ident_old = args - .get(ident_idx + 1) - .expect("FATAL: missing identifier for old run"); - let wspace_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); - let runs_dir = PathBuf::from(&wspace_dir) - .join("..") - .join("..") - .join(consts::RUNS_DIR); - - // Use the blank outputs initially, so we can merge into these with prefixes. - let mut outputs_new = Output::blank(); - let mut outputs_old = Output::blank(); - - for e in runs_dir.read_dir().unwrap() { - let Ok(entry) = e else { - continue; - }; - let Ok(metadata) = entry.metadata() else { - continue; - }; - if metadata.is_file() { - let Ok(name) = entry.file_name().into_string() else { - continue; - }; - - // A little helper to avoid code duplication. Reads the `output` from - // a json file, then merges it into what we have so far. - let read_into = |output: &mut Output| { - let mut elems = name.split('.').skip(1); - let prefix = elems.next().unwrap(); - assert_eq!("json", elems.next().unwrap()); - assert!(elems.next().is_none()); - let mut buffer = Vec::new(); - let _ = OpenOptions::new() - .read(true) - .open(entry.path()) - .unwrap() - .read_to_end(&mut buffer) - .unwrap(); - let o_other: Output = serde_json::from_slice(&buffer).unwrap(); - output.merge(o_other, prefix); - }; - - if name.starts_with(ident_old) { - read_into(&mut outputs_old); - } else if name.starts_with(ident_new) { - read_into(&mut outputs_new); - } - } - } - - let res = outputs_new.compare_perf(outputs_old); - if let Some(filename) = save_to { - let mut file = std::fs::OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(filename) - .expect("FATAL: couldn't save run results to file"); - file.write_all(format!("{res}").as_bytes()).unwrap(); - } else { - println!("{res}"); - } -} - -/// Runs a test binary, filtering out tests which aren't marked for perf triage -/// and giving back the list of tests we care about. -/// -/// The output of this is an iterator over `test_fn_name, test_mdata_name`. -fn get_tests(t_bin: &str) -> impl ExactSizeIterator { - let mut cmd = Command::new(t_bin); - // --format=json is nightly-only :( - cmd.args(["--list", "--format=terse"]); - let out = cmd - .output() - .expect("FATAL: Could not run test binary {t_bin}"); - assert!( - out.status.success(), - "FATAL: Cannot do perf check - test binary {t_bin} returned an error" - ); - if !QUIET.load(Ordering::Relaxed) { - eprintln!("Test binary ran successfully; starting profile..."); - } - // Parse the test harness output to look for tests we care about. - let stdout = String::from_utf8_lossy(&out.stdout); - let mut test_list: Vec<_> = stdout - .lines() - .filter_map(|line| { - // This should split only in two; e.g., - // "app::test::test_arena: test" => "app::test::test_arena:", "test" - let line: Vec<_> = line.split_whitespace().collect(); - match line[..] { - // Final byte of t_name is ":", which we need to ignore. - [t_name, kind] => (kind == "test").then(|| &t_name[..t_name.len() - 1]), - _ => None, - } - }) - // Exclude tests that aren't marked for perf triage based on suffix. - .filter(|t_name| { - t_name.ends_with(consts::SUF_NORMAL) || t_name.ends_with(consts::SUF_MDATA) - }) - .collect(); - - // Pulling itertools just for .dedup() would be quite a big dependency that's - // not used elsewhere, so do this on a vec instead. - test_list.sort_unstable(); - test_list.dedup(); - - // Tests should come in pairs with their mdata fn! - assert!( - test_list.len().is_multiple_of(2), - "Malformed tests in test binary {t_bin}" - ); - - let out = test_list - .chunks_exact_mut(2) - .map(|pair| { - // Be resilient against changes to these constants. - if consts::SUF_NORMAL < consts::SUF_MDATA { - (pair[0].to_owned(), pair[1].to_owned()) - } else { - (pair[1].to_owned(), pair[0].to_owned()) - } - }) - .collect::>(); - out.into_iter() -} - -/// Runs the specified test `count` times, returning the time taken if the test -/// succeeded. -#[inline] -fn spawn_and_iterate(t_bin: &str, t_name: &str, count: NonZero) -> Option { - let mut cmd = Command::new(t_bin); - cmd.args([t_name, "--exact"]); - cmd.env(consts::ITER_ENV_VAR, format!("{count}")); - // Don't let the child muck up our stdin/out/err. - cmd.stdin(Stdio::null()); - cmd.stdout(Stdio::null()); - cmd.stderr(Stdio::null()); - let pre = Instant::now(); - // Discard the output beyond ensuring success. - let out = cmd.spawn().unwrap().wait(); - let post = Instant::now(); - out.iter().find_map(|s| s.success().then_some(post - pre)) -} - -/// Triage a test to determine the correct number of iterations that it should run. -/// Specifically, repeatedly runs the given test until its execution time exceeds -/// `thresh`, calling `step(iterations)` after every failed run to determine the new -/// iteration count. Returns `None` if the test errored or `step` returned `None`, -/// else `Some(iterations)`. -/// -/// # Panics -/// This will panic if `step(usize)` is not monotonically increasing, or if the test -/// binary is invalid. -fn triage_test( - t_bin: &str, - t_name: &str, - thresh: Duration, - mut step: impl FnMut(NonZero) -> Option>, -) -> Option> { - let mut iter_count = DEFAULT_ITER_COUNT; - // It's possible that the first loop of a test might be an outlier (e.g. it's - // doing some caching), in which case we want to skip it. - let duration_once = spawn_and_iterate(t_bin, t_name, NonZero::new(1).unwrap())?; - loop { - let duration = spawn_and_iterate(t_bin, t_name, iter_count)?; - if duration.saturating_sub(duration_once) > thresh { - break Some(iter_count); - } - let new = step(iter_count)?; - assert!( - new > iter_count, - "FATAL: step must be monotonically increasing" - ); - iter_count = new; - } -} - -/// Try to find the hyperfine binary the user has installed. -fn hyp_binary() -> Option { - const HYP_PATH: &str = "hyperfine"; - const HYP_HOME: &str = "~/.cargo/bin/hyperfine"; - if Command::new(HYP_PATH).output().is_err() { - if Command::new(HYP_HOME).output().is_err() { - None - } else { - Some(Command::new(HYP_HOME)) - } - } else { - Some(Command::new(HYP_PATH)) - } -} - -/// Profiles a given test with hyperfine, returning the mean and standard deviation -/// for its runtime. If the test errors, returns `None` instead. -fn hyp_profile(t_bin: &str, t_name: &str, iterations: NonZero) -> Option { - let mut perf_cmd = hyp_binary().expect("Couldn't find the Hyperfine binary on the system"); - - // Warm up the cache and print markdown output to stdout, which we parse. - perf_cmd.args([ - "--style", - "none", - "--warmup", - "1", - "--export-markdown", - "-", - // Parse json instead... - "--time-unit", - "millisecond", - &format!("{t_bin} --exact {t_name}"), - ]); - perf_cmd.env(consts::ITER_ENV_VAR, format!("{iterations}")); - let p_out = perf_cmd.output().unwrap(); - if !p_out.status.success() { - return None; - } - - let cmd_output = String::from_utf8_lossy(&p_out.stdout); - // Can't use .last() since we have a trailing newline. Sigh. - let results_line = cmd_output.lines().nth(3).unwrap(); - // Grab the values out of the pretty-print. - // TODO: Parse json instead. - let mut res_iter = results_line.split_whitespace(); - // Durations are given in milliseconds, so account for that. - let mean = Duration::from_secs_f64(res_iter.nth(5).unwrap().parse::().unwrap() / 1000.); - let stddev = Duration::from_secs_f64(res_iter.nth(1).unwrap().parse::().unwrap() / 1000.); - - Some(Timings { mean, stddev }) -} - -fn main() { - let args = std::env::args().collect::>(); - // We get passed the test we need to run as the 1st argument after our own name. - let t_bin = args - .get(1) - .expect("FATAL: No test binary or command; this shouldn't be manually invoked!"); - - // We're being asked to compare two results, not run the profiler. - if t_bin == "compare" { - compare_profiles(&args[2..]); - return; - } - - // Minimum test importance we care about this run. - let mut thresh = Importance::Iffy; - // Where to print the output of this run. - let mut out_kind = OutputKind::Markdown; - - for arg in args.iter().skip(2) { - match arg.as_str() { - "--critical" => thresh = Importance::Critical, - "--important" => thresh = Importance::Important, - "--average" => thresh = Importance::Average, - "--iffy" => thresh = Importance::Iffy, - "--fluff" => thresh = Importance::Fluff, - "--quiet" => QUIET.store(true, Ordering::Relaxed), - s if s.starts_with("--json") => { - out_kind = OutputKind::Json(Path::new( - s.strip_prefix("--json=") - .expect("FATAL: Invalid json parameter; pass --json=ident"), - )); - } - _ => (), - } - } - if !QUIET.load(Ordering::Relaxed) { - eprintln!("Starting perf check"); - } - - let mut output = Output::default(); - - // Spawn and profile an instance of each perf-sensitive test, via hyperfine. - // Each test is a pair of (test, metadata-returning-fn), so grab both. We also - // know the list is sorted. - let i = get_tests(t_bin); - let len = i.len(); - for (idx, (ref t_name, ref t_mdata)) in i.enumerate() { - if !QUIET.load(Ordering::Relaxed) { - eprint!("\rProfiling test {}/{}", idx + 1, len); - } - // Pretty-printable stripped name for the test. - let t_name_pretty = t_name.replace(consts::SUF_NORMAL, ""); - - // Get the metadata this test reports for us. - let t_mdata = match parse_mdata(t_bin, t_mdata) { - Ok(mdata) => mdata, - Err(err) => fail!(output, t_name_pretty, err), - }; - - if t_mdata.importance < thresh { - fail!(output, t_name_pretty, t_mdata, FailKind::Skipped); - } - - // Time test execution to see how many iterations we need to do in order - // to account for random noise. This is skipped for tests with fixed - // iteration counts. - let final_iter_count = t_mdata.iterations.or_else(|| { - triage_test(t_bin, t_name, consts::NOISE_CUTOFF, |c| { - if let Some(c) = c.checked_mul(ITER_COUNT_MUL) { - Some(c) - } else { - // This should almost never happen, but maybe..? - eprintln!( - "WARNING: Ran nearly usize::MAX iterations of test {t_name_pretty}; skipping" - ); - None - } - }) - }); - - // Don't profile failing tests. - let Some(final_iter_count) = final_iter_count else { - fail!(output, t_name_pretty, t_mdata, FailKind::Triage); - }; - - // Now profile! - if let Some(timings) = hyp_profile(t_bin, t_name, final_iter_count) { - output.success(t_name_pretty, t_mdata, final_iter_count, timings); - } else { - fail!( - output, - t_name_pretty, - t_mdata, - final_iter_count, - FailKind::Profile - ); - } - } - if !QUIET.load(Ordering::Relaxed) { - if output.is_empty() { - eprintln!("Nothing to do."); - } else { - // If stdout and stderr are on the same terminal, move us after the - // output from above. - eprintln!(); - } - } - - // No need making an empty json file on every empty test bin. - if output.is_empty() { - return; - } - - out_kind.log(&output, t_bin); -}