diff --git a/.cargo/ci-config.toml b/.cargo/ci-config.toml index b31b79a59b..7a740adfd7 100644 --- a/.cargo/ci-config.toml +++ b/.cargo/ci-config.toml @@ -14,15 +14,3 @@ rustflags = ["-D", "warnings"] # We don't need fullest debug information for dev stuff (tests etc.) in CI. [profile.dev] debug = "limited" - -# Use Mold on Linux, because it's faster than GNU ld and LLD. -# -# We no longer set this in the default `config.toml` so that developers can opt in to Wild, which -# is faster than Mold, in their own ~/.cargo/config.toml. -[target.x86_64-unknown-linux-gnu] -linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold"] - -[target.aarch64-unknown-linux-gnu] -linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold"] diff --git a/.cargo/config.toml b/.cargo/config.toml index 9b2e6f51c9..a9bf1f9cc9 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -16,5 +16,9 @@ rustflags = [ "target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows ] +# We need lld to link libwebrtc.a successfully on aarch64-linux +[target.aarch64-unknown-linux-gnu] +rustflags = ["-C", "link-arg=-fuse-ld=lld"] + [env] MACOSX_DEPLOYMENT_TARGET = "10.15.7" diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index 2641db6183..0000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(cargo build:*)" - ] - } -} diff --git a/.config/nextest.toml b/.config/nextest.toml deleted file mode 100644 index e8fa9bdf9d..0000000000 --- a/.config/nextest.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Nextest configuration for GPUI -# https://nexte.st/book/configuration.html - -[profile.default] -# Default test settings \ No newline at end of file diff --git a/.gitignore b/.gitignore index d3157ef5a5..3a74e1b6b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,13 @@ -# Build artifacts +# Build Artifacts **/target **/cargo-target *.wasm -# IDE and editor +# IDE and Editor .DS_Store .idea .vscode +.zed *.xcodeproj *.xcworkspace DerivedData/ @@ -23,11 +24,14 @@ xcuserdata/ .envrc /result -# Environment and secrets +# Environment and Secrets .env .env.secret.toml .netrc +# Version Control +.git + # Misc **/*.db .build diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index 4f7a5a6245..0000000000 --- a/.zed/settings.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "languages": { - "Markdown": { - "tab_size": 2, - }, - "TOML": { - "format_on_save": "off", - }, - "YAML": { - "tab_size": 2, - }, - "JSON": { - "tab_size": 2, - "preferred_line_length": 120, - }, - "JSONC": { - "tab_size": 2, - "preferred_line_length": 120, - }, - }, - "hard_tabs": false, - "formatter": "auto", - "remove_trailing_whitespace_on_save": true, - "ensure_final_newline_on_save": true, - "file_scan_exclusions": [ - "**/.git", - "**/.svn", - "**/.hg", - "**/.jj", - "**/CVS", - "**/.DS_Store", - "**/Thumbs.db", - "**/target", - ], -} diff --git a/.zed/tasks.json b/.zed/tasks.json deleted file mode 100644 index 57f6b28e12..0000000000 --- a/.zed/tasks.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "label": "clippy", - "command": "./script/clippy", - "args": [], - "allow_concurrent_runs": true, - "use_new_terminal": false, - }, - { - "label": "cargo check gpui", - "command": "cargo", - "args": ["check", "--package", "gpui"], - "allow_concurrent_runs": true, - "use_new_terminal": false, - }, - { - "label": "cargo test gpui", - "command": "cargo", - "args": ["test", "--package", "gpui"], - "allow_concurrent_runs": false, - "use_new_terminal": false, - }, -] diff --git a/Cargo.lock b/Cargo.lock index 0096b90e01..969297fc04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", + "zeroize", +] + [[package]] name = "ahash" version = "0.8.12" @@ -233,6 +245,31 @@ dependencies = [ "libloading", ] +[[package]] +name = "ashpd" +version = "0.13.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13bdf0fd848239dcd5e64eeeee35dbc00378ebcc6f3aa4ead0a305eec83d0cfb" +dependencies = [ + "enumflags2", + "futures-util", + "getrandom 0.4.1", + "serde", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -366,6 +403,17 @@ dependencies = [ "rustix 1.1.4", ] +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-signal" version = "0.2.13" @@ -417,6 +465,17 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async_zip" version = "0.0.17" @@ -569,13 +628,31 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", ] [[package]] @@ -677,6 +754,15 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.2.56" @@ -707,6 +793,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + [[package]] name = "chacha20" version = "0.10.0" @@ -718,12 +813,66 @@ dependencies = [ "rand_core 0.10.0", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "circular-buffer" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c638459986b83c2b885179bd4ea6a2cbb05697b001501a56adb3a3d230803b" +[[package]] +name = "cocoa" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" +dependencies = [ + "bitflags 2.11.0", + "block", + "cocoa-foundation", + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "libc", + "objc", +] + [[package]] name = "codespan-reporting" version = "0.12.0" @@ -821,9 +970,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" dependencies = [ "core-foundation-sys", "libc", @@ -848,6 +997,19 @@ dependencies = [ "libc", ] +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.0", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.1.3" @@ -866,7 +1028,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ "bitflags 2.11.0", - "core-foundation 0.10.1", + "core-foundation 0.10.0", + "libc", +] + +[[package]] +name = "core-graphics2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4583956b9806b69f73fcb23aee05eb3620efc282972f08f6a6db7504f8334d" +dependencies = [ + "bitflags 2.11.0", + "block", + "cfg-if", + "core-foundation 0.10.0", "libc", ] @@ -877,11 +1052,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" dependencies = [ "core-foundation 0.9.4", - "core-graphics", + "core-graphics 0.23.2", "foreign-types", "libc", ] +[[package]] +name = "core-text" +version = "21.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" +dependencies = [ + "core-foundation 0.10.0", + "core-graphics 0.24.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-video" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45e71d5be22206bed53c3c3cb99315fc4c3d31b8963808c6bc4538168c4f8ef" +dependencies = [ + "block", + "core-foundation 0.10.0", + "core-graphics2", + "io-surface", + "libc", + "metal 0.29.0", +] + [[package]] name = "core2" version = "0.4.0" @@ -1085,6 +1286,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -1134,6 +1336,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1229,6 +1443,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_filter" version = "1.0.0" @@ -1424,6 +1665,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1491,8 +1738,8 @@ dependencies = [ "bitflags 2.11.0", "byteorder", "core-foundation 0.9.4", - "core-graphics", - "core-text", + "core-graphics 0.23.2", + "core-text 20.1.0", "dirs 6.0.0", "dwrote", "float-ord", @@ -1630,6 +1877,19 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite 2.6.1", + "pin-project", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.32" @@ -1907,19 +2167,32 @@ name = "gpui-ce" version = "0.3.3" dependencies = [ "anyhow", + "async-channel 2.5.0", "async-task", "backtrace", + "block", "bytemuck", + "chrono", "circular-buffer", + "cocoa", + "cocoa-foundation", + "core-foundation 0.10.0", + "core-foundation-sys", + "core-graphics 0.24.0", + "core-text 21.0.0", + "core-video", "cosmic-text", "ctor", "derive_more 2.1.1", + "dispatch2", "env_logger", "etagere", "flume", "font-kit", + "foreign-types", "futures", - "gpui-macros", + "futures-concurrency", + "gpui-ce-macros", "gpui_collections", "gpui_http_client", "gpui_refineable", @@ -1932,7 +2205,13 @@ dependencies = [ "libc", "log", "lyon", + "mach2", + "metal 0.29.0", "num_cpus", + "objc", + "objc2 0.6.4", + "objc2-metal 0.3.2", + "oo7", "parking", "parking_lot", "pathfinder_geometry", @@ -1942,6 +2221,7 @@ dependencies = [ "pretty_assertions", "priority-threadpool", "profiling", + "proptest", "rand 0.10.0", "raw-window-handle", "resvg", @@ -1956,22 +2236,26 @@ dependencies = [ "spin 0.10.0", "stacksafe", "strum", + "swash", "taffy", "thiserror 2.0.18", "unicode-segmentation", "usvg", "uuid", "waker-fn", + "web-time", "wgpu", + "windows-core 0.61.2", + "windows-numerics 0.2.0", + "windows-registry 0.5.3", "winit", ] [[package]] -name = "gpui-macros" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcb02dd63a2859714ac7b6b476937617c3c744157af1b49f7c904023a79039be" +name = "gpui-ce-macros" +version = "0.1.0" dependencies = [ + "gpui-ce", "heck", "proc-macro2", "quote", @@ -2188,12 +2472,36 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hexf-parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "home" version = "0.5.12" @@ -2301,6 +2609,30 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +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.1.1" @@ -2440,7 +2772,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" dependencies = [ "byteorder-lite", - "quick-error", + "quick-error 2.0.1", ] [[package]] @@ -2467,6 +2799,16 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -2496,6 +2838,18 @@ dependencies = [ "rustversion", ] +[[package]] +name = "io-surface" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "554b8c5d64ec09a3a520fe58e4d48a73e00ff32899cdcbe32a4877afd4968b8e" +dependencies = [ + "cgl", + "core-foundation 0.10.0", + "core-foundation-sys", + "leaky-cow", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2632,6 +2986,21 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd100e01f1154f2908dfa7d02219aeab25d0b9c7fa955164192e3245255a0c73" + +[[package]] +name = "leaky-cow" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a8225d44241fd324a8af2806ba635fc7c8a7e9a7de4d5cf3ef54e71f5926fc" +dependencies = [ + "leak", +] + [[package]] name = "leb128fmt" version = "0.1.0" @@ -2852,6 +3221,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea" +dependencies = [ + "libc", +] + [[package]] name = "malloc_buf" version = "0.0.6" @@ -2871,6 +3249,16 @@ dependencies = [ "rayon", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.8.0" @@ -2886,6 +3274,30 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-graphics-types 0.1.3", + "foreign-types", + "log", + "objc", + "paste", +] + [[package]] name = "metal" version = "0.33.0" @@ -3049,6 +3461,20 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3059,6 +3485,32 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f9a86e097b0d187ad0e65667c2f58b9254671e86e7dbb78036b16692eae099" +dependencies = [ + "libm", + "num-integer", + "num-iter", + "num-traits", + "once_cell", + "rand 0.9.2", + "serde", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-derive" version = "0.4.2" @@ -3079,6 +3531,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -3157,6 +3620,15 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-app-kit" version = "0.2.2" @@ -3164,12 +3636,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] @@ -3180,10 +3652,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3192,9 +3664,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3204,9 +3676,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", ] [[package]] @@ -3215,10 +3698,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", ] [[package]] @@ -3227,10 +3710,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-contacts", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3246,10 +3729,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ "bitflags 2.11.0", - "block2", + "block2 0.5.1", "dispatch", "libc", - "objc2", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", ] [[package]] @@ -3258,10 +3751,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" dependencies = [ - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3271,9 +3764,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", ] [[package]] @@ -3283,10 +3790,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", - "objc2-foundation", - "objc2-metal", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", ] [[package]] @@ -3295,8 +3802,8 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" dependencies = [ - "objc2", - "objc2-foundation", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3306,13 +3813,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-cloud-kit", "objc2-core-data", "objc2-core-image", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-link-presentation", "objc2-quartz-core", "objc2-symbols", @@ -3326,9 +3833,9 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -3338,10 +3845,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ "bitflags 2.11.0", - "block2", - "objc2", + "block2 0.5.1", + "objc2 0.5.2", "objc2-core-location", - "objc2-foundation", + "objc2-foundation 0.2.2", ] [[package]] @@ -3365,6 +3872,41 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oo7" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78f2bfed90f1618b4b48dcad9307f25e14ae894e2949642c87c351601d62cebd" +dependencies = [ + "aes", + "ashpd", + "async-fs", + "async-io", + "async-lock", + "blocking", + "cbc", + "cipher", + "digest", + "endi", + "futures-lite 2.6.1", + "futures-util", + "getrandom 0.4.1", + "hkdf", + "hmac", + "md-5", + "num", + "num-bigint-dig", + "pbkdf2", + "serde", + "serde_bytes", + "sha2", + "subtle", + "zbus", + "zbus_macros", + "zeroize", + "zvariant", +] + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3396,6 +3938,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "owned-alloc" version = "0.2.0" @@ -3471,6 +4023,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3715,6 +4277,25 @@ dependencies = [ "syn", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.11.0", + "num-traits", + "rand 0.9.2", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "psm" version = "0.1.30" @@ -3743,6 +4324,12 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quick-error" version = "2.0.1" @@ -3815,9 +4402,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3874,6 +4461,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "range-alloc" version = "0.1.5" @@ -3930,7 +4526,7 @@ dependencies = [ "avif-serialize", "imgref", "loop9", - "quick-error", + "quick-error 2.0.1", "rav1e", "rayon", "rgb", @@ -4298,6 +4894,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error 1.2.3", + "tempfile", + "wait-timeout", +] + [[package]] name = "rustybuzz" version = "0.20.1" @@ -4404,7 +5012,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", - "core-foundation 0.10.1", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -4446,6 +5054,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4513,6 +5131,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -5071,7 +5700,7 @@ dependencies = [ "fax", "flate2", "half", - "quick-error", + "quick-error 2.0.1", "weezl", "zune-jpeg 0.4.21", ] @@ -5265,9 +5894,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -5304,6 +5945,23 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicase" version = "2.9.0" @@ -5512,6 +6170,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "waker-fn" version = "1.2.0" @@ -5917,7 +6584,7 @@ dependencies = [ "libc", "libloading", "log", - "metal", + "metal 0.33.0", "naga", "ndk-sys", "objc", @@ -5936,7 +6603,7 @@ dependencies = [ "web-sys", "wgpu-types", "windows", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -6002,9 +6669,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.62.2", "windows-future", - "windows-numerics", + "windows-numerics 0.3.1", ] [[package]] @@ -6013,7 +6680,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core", + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -6035,7 +6715,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core", + "windows-core 0.62.2", "windows-link 0.2.1", "windows-threading", ] @@ -6074,13 +6754,23 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-numerics" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core", + "windows-core 0.62.2", "windows-link 0.2.1", ] @@ -6095,6 +6785,17 @@ dependencies = [ "windows-targets 0.53.5", ] +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -6122,6 +6823,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-strings" version = "0.5.1" @@ -6381,22 +7091,22 @@ dependencies = [ "android-activity", "atomic-waker", "bitflags 2.11.0", - "block2", + "block2 0.5.1", "bytemuck", "calloop", "cfg_aliases", "concurrent-queue", "core-foundation 0.9.4", - "core-graphics", + "core-graphics 0.23.2", "cursor-icon", "dpi", "js-sys", "libc", "memmap2", "ndk", - "objc2", + "objc2 0.5.2", "objc2-app-kit", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", "percent-encoding", @@ -6671,6 +7381,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener 5.4.1", + "futures-core", + "futures-lite 2.6.1", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zed-async-tar" version = "0.5.0-zed" @@ -6732,7 +7503,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry", + "windows-registry 0.4.0", ] [[package]] @@ -6787,6 +7558,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -6865,3 +7650,44 @@ checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ "zune-core 0.5.1", ] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "serde_bytes", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index 92d9e54864..13a5c8ce94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,13 @@ +[workspace] +members = [ + ".", + "tooling/macros", +] +default-members = [ + ".", +] +resolver = "3" + [package] name = "gpui-ce" version = "0.3.3" @@ -14,15 +24,18 @@ categories = ["gui"] autoexamples = false [features] -default = ["font-kit"] +default = ["font-kit", "windows-manifest"] test-support = [ "leak-detection", + "backtrace", "collections/test-support", "util/test-support", "http_client/test-support", ] -inspector = ["gpui_macros/inspector"] +inspector = ["gpui-ce-macros/inspector"] leak-detection = ["backtrace"] +runtime_shaders = [] +windows-manifest = [] [lib] name = "gpui" @@ -30,25 +43,21 @@ path = "src/gpui.rs" doctest = false [dependencies] -pollster = "0.4.0" -winit = "0.30.12" -wgpu = "28" -priority-threadpool = { git = "https://github.com/mdeand/priority-threadpool" } anyhow = "1.0.86" +async-channel = "2" async-task = "4.7" backtrace = { version = "0.3", optional = true } bytemuck = { version = "1.24", features = ["derive"] } +chrono = "0.4" circular-buffer = "1.0" collections = { package = "gpui_collections", version = "0.2.2" } cosmic-text = "0.18.2" ctor = "0.6.3" derive_more = { version = "2.1.1", features = ["full"] } etagere = "0.2" -flume = "0.12" -# font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true } -font-kit = { version = "0.14.3", optional = true } futures = "0.3" -gpui_macros = { package = "gpui-macros", version = "0.2.2" } +futures-concurrency = "7" +gpui-ce-macros = { path = "./tooling/macros", features = ["inspector"] } http_client = { package = "gpui_http_client", version = "0.2.2" } image = "0.25.1" inventory = "0.3.19" @@ -59,10 +68,11 @@ lyon = "1.0" num_cpus = "1.13" parking = "2.0.0" parking_lot = "0.12.1" -pathfinder_geometry = "0.5" pin-project = "1.1.10" +pollster = "0.4.0" postage = { version = "0.5", features = ["futures-traits"] } profiling = "1" +priority-threadpool = { git = "https://github.com/mdeand/priority-threadpool" } rand = "0.10" raw-window-handle = "0.6" refineable = { package = "gpui_refineable", version = "0.2.2" } @@ -90,19 +100,54 @@ util_macros = { package = "gpui_util_macros", version = "0.2.2" } usvg = { version = "0.47.0", default-features = false } uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } waker-fn = "1.2.0" +web-time = "1" +wgpu = "28" +winit = "0.30.12" + +[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"] } +dispatch2 = "0.3.1" +flume = "0.12" +# font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true } +font-kit = { version = "0.14.3", optional = true } +foreign-types = "0.5" +mach2 = "0.5" +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"))'.dependencies] +flume = "0.12" +oo7 = { version = "0.6", default-features = false, features = [ + "async-std", + "native_crypto", +] } +swash = "0.2.6" + +[target.'cfg(target_os = "windows")'.dependencies] +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", -] } +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", -] } +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.10" +proptest = "1" unicode-segmentation = "1.10" util = { package = "gpui_util", version = "0.2.2", features = ["test-support"] } diff --git a/LICENSE-APACHE b/LICENSE-APACHE deleted file mode 100644 index 461a0fe5ba..0000000000 --- a/LICENSE-APACHE +++ /dev/null @@ -1,222 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - - http://www.apache.org/licenses/LICENSE-2.0 - - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - - - -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - - 1. Definitions. - - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - - END OF TERMS AND CONDITIONS diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..ed7ebb21b0 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,169 @@ +# Apache License + +_Version 2.0, January 2004_ +_<>_ + +### Terms and Conditions for use, reproduction, and distribution + +#### 1. Definitions + +“License” shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +“Licensor” shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +“Legal Entity” shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, “control” means **(i)** the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the +outstanding shares, or **(iii)** beneficial ownership of such entity. + +“You” (or “Your”) shall mean an individual or Legal Entity exercising +permissions granted by this License. + +“Source” form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +“Object” form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +“Work” shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +“Derivative Works” shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +“Contribution” shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +“submitted” means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as “Not a Contribution.” + +“Contributor” shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +#### 2. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +#### 3. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +#### 4. Redistribution + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +- **(a)** You must give any other recipients of the Work or Derivative Works a copy of + this License; and +- **(b)** You must cause any modified files to carry prominent notices stating that You + changed the files; and +- **(c)** You must retain, in the Source form of any Derivative Works that You distribute, + all copyright, patent, trademark, and attribution notices from the Source form + of the Work, excluding those notices that do not pertain to any part of the + Derivative Works; and +- **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any + Derivative Works that You distribute must include a readable copy of the + attribution notices contained within such NOTICE file, excluding those notices + that do not pertain to any part of the Derivative Works, in at least one of the + following places: within a NOTICE text file distributed as part of the + Derivative Works; within the Source form or documentation, if provided along + with the Derivative Works; or, within a display generated by the Derivative + Works, if and wherever such third-party notices normally appear. The contents of + the NOTICE file are for informational purposes only and do not modify the + License. You may add Your own attribution notices within Derivative Works that + You distribute, alongside or as an addendum to the NOTICE text from the Work, + provided that such additional attribution notices cannot be construed as + modifying the License. + +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +#### 5. Submission of Contributions + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +#### 6. Trademarks + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +#### 7. Disclaimer of Warranty + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +#### 8. Limitation of Liability + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +#### 9. Accepting Warranty or Additional Liability + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +_END OF TERMS AND CONDITIONS_ diff --git a/examples/learn/wgpu_surface.rs b/examples/learn/wgpu_surface.rs index b8e1ed548a..d840f24234 100644 --- a/examples/learn/wgpu_surface.rs +++ b/examples/learn/wgpu_surface.rs @@ -36,7 +36,7 @@ struct SurfaceExample { } impl Render for SurfaceExample { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, _cx: &mut Context) -> impl IntoElement { // pull any pending fps samples from channel while let Ok(f) = self.fps_rx.try_recv() { self.display_fps = f; @@ -323,7 +323,7 @@ fn fs_main(in: VSOut) -> @location(0) vec4 { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, view_formats: &[], }); - let depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default()); + let _depth_view = depth_tex.create_view(&wgpu::TextureViewDescriptor::default()); let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor{ label: Some("CubeUniformBuf"), @@ -374,6 +374,7 @@ fn fs_main(in: VSOut) -> @location(0) vec4 { label: Some("SurfaceExample Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment{ view: &view, + depth_slice: None, resolve_target: None, ops: wgpu::Operations{ load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), @@ -388,6 +389,7 @@ fn fs_main(in: VSOut) -> @location(0) vec4 { }), stencil_ops: None, }), + multiview_mask: None, occlusion_query_set: None, timestamp_writes: None, }); diff --git a/examples/legacy/window.rs b/examples/legacy/window.rs index 06003c4663..b5159a36ea 100644 --- a/examples/legacy/window.rs +++ b/examples/legacy/window.rs @@ -1,5 +1,5 @@ use gpui::{ - App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Timer, Window, + App, Application, Bounds, Context, KeyBinding, PromptButton, PromptLevel, Window, WindowBounds, WindowKind, WindowOptions, actions, div, prelude::*, px, rgb, size, }; @@ -188,7 +188,7 @@ impl Render for WindowDemo { // Restore the application after 3 seconds window .spawn(cx, async move |cx| { - Timer::after(std::time::Duration::from_secs(3)).await; + smol::Timer::after(std::time::Duration::from_secs(3)).await; cx.update(|_, cx| { cx.activate(false); }) diff --git a/flake.lock b/flake.lock index a61915afba..d8bbb50ac0 100644 --- a/flake.lock +++ b/flake.lock @@ -1,19 +1,33 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1773857772, + "narHash": "sha256-5xsK26KRHf0WytBtsBnQYC/lTWDhQuT57HJ7SzuqZcM=", + "owner": "ipetkov", + "repo": "crane", + "rev": "b556d7bbae5ff86e378451511873dfd07e4504cd", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "fenix": { "inputs": { "nixpkgs": [ - "naersk", "nixpkgs" ], "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1752475459, - "narHash": "sha256-z6QEu4ZFuHiqdOPbYss4/Q8B0BFhacR8ts6jO/F/aOU=", + "lastModified": 1774076307, + "narHash": "sha256-v8axK9HGgVERw9oG3SKdsuE+ri0GPUZDyRBN4GLqQ1c=", "owner": "nix-community", "repo": "fenix", - "rev": "bf0d6f70f4c9a9cf8845f992105652173f4b617f", + "rev": "556198cc6c69c0a13228a15e33b2360f333b0092", "type": "github" }, "original": { @@ -40,64 +54,13 @@ "type": "github" } }, - "naersk": { - "inputs": { - "fenix": "fenix", - "nixpkgs": "nixpkgs" - }, - "locked": { - "lastModified": 1769799857, - "narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=", - "owner": "nix-community", - "repo": "naersk", - "rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "naersk", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1752077645, - "narHash": "sha256-HM791ZQtXV93xtCY+ZxG1REzhQenSQO020cu6rHtAPk=", + "lastModified": 1773840656, + "narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "be9e214982e20b8310878ac2baa063a961c1bdf6", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1770141374, - "narHash": "sha256-yD4K/vRHPwXbJf5CK3JkptBA6nFWUKNX/jlFp2eKEQc=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "41965737c1797c1d83cfb0b644ed0840a6220bd1", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_3": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512", "type": "github" }, "original": { @@ -109,20 +72,20 @@ }, "root": { "inputs": { + "crane": "crane", + "fenix": "fenix", "flake-utils": "flake-utils", - "naersk": "naersk", - "nixpkgs": "nixpkgs_2", - "rust-overlay": "rust-overlay" + "nixpkgs": "nixpkgs" } }, "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1752428706, - "narHash": "sha256-EJcdxw3aXfP8Ex1Nm3s0awyH9egQvB2Gu+QEnJn2Sfg=", + "lastModified": 1774036669, + "narHash": "sha256-EWhsBSh/h1VcyLKXuTEyH8lNVB2ktuKVkqx8dkQ6hxk=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "591e3b7624be97e4443ea7b5542c191311aa141d", + "rev": "0cf3e8a07f0e29825f5db78840e646a4bb519742", "type": "github" }, "original": { @@ -132,24 +95,6 @@ "type": "github" } }, - "rust-overlay": { - "inputs": { - "nixpkgs": "nixpkgs_3" - }, - "locked": { - "lastModified": 1770174315, - "narHash": "sha256-GUaMxDmJB1UULsIYpHtfblskVC6zymAaQ/Zqfo+13jc=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "095c394bb91342882f27f6c73f64064fb9de9f2a", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, "systems": { "locked": { "lastModified": 1681028828, diff --git a/flake.nix b/flake.nix index 1bc7e7c151..0632bdfc4e 100644 --- a/flake.nix +++ b/flake.nix @@ -1,100 +1,121 @@ { + description = "Standalone GPUI build"; + inputs = { - flake-utils.url = "github:numtide/flake-utils"; - naersk.url = "github:nix-community/naersk"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - rust-overlay.url = "github:oxalica/rust-overlay"; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + crane.url = "github:ipetkov/crane"; + flake-utils.url = "github:numtide/flake-utils"; }; outputs = { self, - flake-utils, - naersk, nixpkgs, - rust-overlay, + fenix, + crane, + flake-utils, }: flake-utils.lib.eachDefaultSystem ( system: let - pkgs = (import nixpkgs) { - inherit system; - overlays = [ - (import rust-overlay) - ]; - }; + pkgs = nixpkgs.legacyPackages.${system}; + inherit (pkgs) lib; + + toolchain = fenix.packages.${system}.latest.withComponents [ + "cargo" + "rustc" + "rust-src" + "rustfmt" + "clippy" + ]; - naersk' = pkgs.callPackage naersk { }; + craneLib = (crane.mkLib pkgs).overrideToolchain toolchain; - buildInputs = with pkgs; [ + src = lib.cleanSourceWith { + src = craneLib.path ./.; + filter = + path: type: + (craneLib.filterCargoSources path type) + || (lib.hasSuffix ".metal" path) + || (lib.hasSuffix ".wgsl" path) + || (lib.hasSuffix ".hlsl" path) + || (lib.hasSuffix ".glsl" path); + }; + + linuxLibs = with pkgs; [ + alsa-lib + libdrm + mesa # provides libgbm libxkbcommon - wayland + libva vulkan-loader + wayland xorg.libX11 - xorg.libXcursor - xorg.libXi - xorg.libXrandr xorg.libxcb - xorg.libXrender - xorg.libXfixes - fontconfig - freetype - openssl - libgit2 - alsa-lib - zlib - stdenv.cc.cc.lib ]; - nativeBuildInputs = with pkgs; [ - (pkgs.rust-bin.stable.latest.default.override { - extensions = [ - "rust-src" - "cargo" - "rustc" - ]; - }) - pkg-config - cmake - perl - python3 - ]; - in - rec { - devShell = pkgs.mkShell { - RUST_SRC_PATH = "${ - pkgs.rust-bin.stable.latest.default.override { - extensions = [ "rust-src" ]; - } - }/lib/rustlib/src/rust/library"; - - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath buildInputs; - XDG_SESSION_TYPE = "wayland"; - shellHook = '' - export WAYLAND_DISPLAY=''${WAYLAND_DISPLAY:-wayland-0} - export XDG_RUNTIME_DIR=''${XDG_RUNTIME_DIR:-/run/user/$(id -u)} - export VK_LAYER_PATH="${pkgs.renderdoc}/lib:${pkgs.renderdoc}/lib64:${pkgs.renderdoc}/share/vulkan/implicit_layer.d:$VK_LAYER_PATH" - export VK_INSTANCE_LAYERS="VK_LAYER_RENDERDOC_Capture:$VK_INSTANCE_LAYERS" - ''; + commonArgs = { + pname = "gpui-ce"; + version = "0.3.3"; - nativeBuildInputs = + inherit src; + strictDeps = true; + + nativeBuildInputs = with pkgs; [ + cmake + pkg-config + rustPlatform.bindgenHook + ]; + + buildInputs = with pkgs; [ - nixfmt - cmake - rustc - rustfmt - cargo - clippy - rust-analyzer - vulkan-tools - vulkan-loader - vulkan-validation-layers - renderdoc + fontconfig + freetype + openssl + zlib ] - ++ buildInputs - ++ nativeBuildInputs; + ++ lib.optionals stdenv.isLinux linuxLibs + ++ lib.optionals stdenv.isDarwin [ + apple-sdk_15 + (darwinMinVersionHook "11.0") + ]; + + env = lib.optionalAttrs pkgs.stdenv.isLinux { + LD_LIBRARY_PATH = lib.makeLibraryPath linuxLibs; + }; + + cargoExtraArgs = "--features runtime_shaders"; + }; + + cargoArtifacts = craneLib.buildDepsOnly commonArgs; + + gpui = craneLib.buildPackage ( + commonArgs + // { + inherit cargoArtifacts; + } + ); + in + { + packages.default = gpui; + + devShells.default = pkgs.mkShell { + inputsFrom = [ gpui ]; + packages = [ toolchain ]; + + shellHook = '' + export RUST_BACKTRACE=1 + export RUST_SRC_PATH="${toolchain}/lib/rustlib/src/rust/library" + ${lib.optionalString pkgs.stdenv.isLinux '' + export LD_LIBRARY_PATH="${lib.makeLibraryPath linuxLibs}:$LD_LIBRARY_PATH" + ''} + ''; }; } ); -} \ No newline at end of file +} diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf b/resources/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf similarity index 100% rename from assets/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf rename to resources/fonts/ibm-plex-sans/IBMPlexSans-Bold.ttf diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf b/resources/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf similarity index 100% rename from assets/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf rename to resources/fonts/ibm-plex-sans/IBMPlexSans-BoldItalic.ttf diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf b/resources/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf similarity index 100% rename from assets/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf rename to resources/fonts/ibm-plex-sans/IBMPlexSans-Italic.ttf diff --git a/assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf b/resources/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf similarity index 100% rename from assets/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf rename to resources/fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf diff --git a/assets/fonts/ibm-plex-sans/license.txt b/resources/fonts/ibm-plex-sans/license.txt similarity index 100% rename from assets/fonts/ibm-plex-sans/license.txt rename to resources/fonts/ibm-plex-sans/license.txt diff --git a/assets/fonts/lilex/Lilex-Bold.ttf b/resources/fonts/lilex/Lilex-Bold.ttf similarity index 100% rename from assets/fonts/lilex/Lilex-Bold.ttf rename to resources/fonts/lilex/Lilex-Bold.ttf diff --git a/assets/fonts/lilex/Lilex-BoldItalic.ttf b/resources/fonts/lilex/Lilex-BoldItalic.ttf similarity index 100% rename from assets/fonts/lilex/Lilex-BoldItalic.ttf rename to resources/fonts/lilex/Lilex-BoldItalic.ttf diff --git a/assets/fonts/lilex/Lilex-Italic.ttf b/resources/fonts/lilex/Lilex-Italic.ttf similarity index 100% rename from assets/fonts/lilex/Lilex-Italic.ttf rename to resources/fonts/lilex/Lilex-Italic.ttf diff --git a/assets/fonts/lilex/Lilex-Regular.ttf b/resources/fonts/lilex/Lilex-Regular.ttf similarity index 100% rename from assets/fonts/lilex/Lilex-Regular.ttf rename to resources/fonts/lilex/Lilex-Regular.ttf diff --git a/assets/fonts/lilex/OFL.txt b/resources/fonts/lilex/OFL.txt similarity index 100% rename from assets/fonts/lilex/OFL.txt rename to resources/fonts/lilex/OFL.txt diff --git a/script/clippy b/scripts/clippy similarity index 100% rename from script/clippy rename to scripts/clippy diff --git a/script/clippy.ps1 b/scripts/clippy.ps1 similarity index 100% rename from script/clippy.ps1 rename to scripts/clippy.ps1 diff --git a/script/linux b/scripts/linux similarity index 100% rename from script/linux rename to scripts/linux diff --git a/script/metal-debug b/scripts/metal-debug similarity index 100% rename from script/metal-debug rename to scripts/metal-debug diff --git a/src/action.rs b/src/action.rs index 38e94aa356..f2ddee97e5 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, Result}; use collections::HashMap; -pub use gpui_macros::Action; +pub use gpui_ce_macros::Action; pub use no_action::{NoAction, is_no_action}; use serde_json::json; use std::{ diff --git a/src/app.rs b/src/app.rs index d95906cecc..3fe0554113 100644 --- a/src/app.rs +++ b/src/app.rs @@ -759,7 +759,7 @@ impl App { let futures = futures::future::join_all(futures); if self - .background_executor + .foreground_executor .block_with_timeout(SHUTDOWN_TIMEOUT, futures) .is_err() { @@ -769,6 +769,12 @@ impl App { self.quitting = false; } + /// Returns a handle to the entity ref counts, used by the test macro for cleanup. + #[doc(hidden)] + pub fn ref_counts_drop_handle(&self) -> impl Sized + use<> { + self.entities.ref_counts_drop_handle() + } + /// Get the id of the current keyboard layout pub fn keyboard_layout(&self) -> &dyn PlatformKeyboardLayout { self.keyboard_layout.as_ref() diff --git a/src/app/entity_map.rs b/src/app/entity_map.rs index 8c1bdfa1ce..1ed2074101 100644 --- a/src/app/entity_map.rs +++ b/src/app/entity_map.rs @@ -60,7 +60,7 @@ pub(crate) struct EntityMap { ref_counts: Arc>, } -struct EntityRefCounts { +pub(crate) struct EntityRefCounts { counts: SlotMap, dropped_entity_ids: Vec, #[cfg(any(test, feature = "leak-detection"))] @@ -84,6 +84,11 @@ impl EntityMap { } } + /// Returns a handle to the ref counts for test cleanup. + pub fn ref_counts_drop_handle(&self) -> Arc> { + self.ref_counts.clone() + } + /// Reserve a slot for an entity, which you can subsequently use with `insert`. pub fn reserve(&self) -> Slot { let id = self.ref_counts.write().counts.insert(1.into()); diff --git a/src/app/test_context.rs b/src/app/test_context.rs index 9e9055055d..9174616369 100644 --- a/src/app/test_context.rs +++ b/src/app/test_context.rs @@ -8,7 +8,6 @@ use crate::{ }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt, channel::oneshot}; -use rand::{SeedableRng, rngs::StdRng}; use std::{ cell::RefCell, future::Future, ops::Deref, path::PathBuf, rc::Rc, sync::Arc, time::Duration, }; @@ -153,7 +152,7 @@ impl TestAppContext { /// Create a single TestAppContext, for non-multi-client tests pub fn single() -> Self { - let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); + let dispatcher = TestDispatcher::new(0); Self::build(dispatcher, None) } diff --git a/src/elements/surface.rs b/src/elements/surface.rs index 7468251918..510ea9802e 100644 --- a/src/elements/surface.rs +++ b/src/elements/surface.rs @@ -10,12 +10,14 @@ pub enum SurfaceSource {} /// A surface element. pub struct Surface { + #[allow(dead_code)] source: SurfaceSource, object_fit: ObjectFit, style: StyleRefinement, } /// Create a new surface element. +#[allow(unreachable_code)] pub fn surface(source: impl Into) -> Surface { Surface { source: source.into(), diff --git a/src/elements/uniform_list.rs b/src/elements/uniform_list.rs index 1e38b0e7ac..766525dc09 100644 --- a/src/elements/uniform_list.rs +++ b/src/elements/uniform_list.rs @@ -712,7 +712,7 @@ mod test { #[gpui::test] fn test_scroll_strategy_nearest(cx: &mut TestAppContext) { use crate::{ - Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, actions, div, + Context, FocusHandle, ScrollStrategy, UniformListScrollHandle, Window, div, prelude::*, px, uniform_list, }; use std::ops::Range; diff --git a/src/executor.rs b/src/executor.rs index a219a20e92..a188b002cd 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,98 +1,35 @@ -use crate::{App, PlatformDispatcher, RunnableMeta, RunnableVariant, TaskTiming, profiler}; -use async_task::Runnable; +use crate::scheduler::Instant; +use crate::scheduler::Scheduler; +use crate::{App, PlatformDispatcher, PlatformScheduler}; use futures::channel::mpsc; -use parking_lot::{Condvar, Mutex}; -use smol::prelude::*; +use futures::prelude::*; use std::{ - fmt::Debug, - marker::PhantomData, - mem::{self, ManuallyDrop}, - num::NonZeroUsize, - panic::Location, - pin::Pin, - rc::Rc, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - task::{Context, Poll}, - thread::{self, ThreadId}, - time::{Duration, Instant}, + fmt::Debug, future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc, + time::Duration, }; use util::TryFutureExt; -use waker_fn::waker_fn; -#[cfg(any(test, feature = "test-support"))] -use rand::rngs::StdRng; +pub use crate::scheduler::{ + FallibleTask, ForegroundExecutor as SchedulerForegroundExecutor, Priority, +}; /// A pointer to the executor that is currently running, /// for spawning background tasks. #[derive(Clone)] pub struct BackgroundExecutor { - #[doc(hidden)] - pub dispatcher: Arc, + inner: crate::scheduler::BackgroundExecutor, + dispatcher: Arc, } /// A pointer to the executor that is currently running, /// for spawning tasks on the main thread. -/// -/// This is intentionally `!Send` via the `not_send` marker field. This is because -/// `ForegroundExecutor::spawn` does not require `Send` but checks at runtime that the future is -/// only polled from the same thread it was spawned from. These checks would fail when spawning -/// foreground tasks from background threads. #[derive(Clone)] pub struct ForegroundExecutor { - #[doc(hidden)] - pub dispatcher: Arc, + inner: crate::scheduler::ForegroundExecutor, + dispatcher: Arc, not_send: PhantomData>, } -/// Realtime task priority -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[repr(u8)] -pub enum RealtimePriority { - /// Audio task - Audio, - /// Other realtime task - #[default] - Other, -} - -/// Task priority -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -#[repr(u8)] -pub enum Priority { - /// Realtime priority - /// - /// Spawning a task with this priority will spin it off on a separate thread dedicated just to that task. - Realtime(RealtimePriority), - /// High priority - /// - /// Only use for tasks that are critical to the user experience / responsiveness of the editor. - High, - /// Medium priority, probably suits most of your use cases. - #[default] - Medium, - /// Low priority - /// - /// Prioritize this for background work that can come in large quantities - /// to not starve the executor of resources for high priority tasks - Low, -} - -impl Priority { - #[allow(dead_code)] - pub(crate) const fn probability(&self) -> u32 { - match self { - // realtime priorities are not considered for probability scheduling - Priority::Realtime(_) => 0, - Priority::High => 60, - Priority::Medium => 30, - Priority::Low => 10, - } - } -} - /// Task is a primitive that allows work to happen in the background. /// /// It implements [`Future`] so you can `.await` on it. @@ -101,39 +38,44 @@ impl Priority { /// 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), -} +pub struct Task(crate::scheduler::Task); impl Task { - /// Creates a new task that will resolve with the value + /// Creates a new task that will resolve with the value. pub fn ready(val: T) -> Self { - Task(TaskState::Ready(Some(val))) + Task(crate::scheduler::Task::ready(val)) } - /// Detaching a task runs it to completion in the background + /// Returns true if the task has completed or was created with `Task::ready`. + pub fn is_ready(&self) -> bool { + self.0.is_ready() + } + + /// 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(), - } + self.0.detach() + } + + /// Wraps a scheduler Task. + pub fn from_scheduler(task: crate::scheduler::Task) -> Self { + Task(task) + } + + /// Converts this task into a fallible task that returns `Option`. + /// + /// Unlike the standard `Task`, a [`FallibleTask`] will return `None` + /// if the task was cancelled. + pub fn fallible(self) -> FallibleTask { + self.0.fallible() } } -impl Task> +impl Task> where T: 'static, E: 'static + Debug, { - /// Run the task to completion in the background and log any - /// errors that occur. + /// Run the task to completion in the background and log any errors that occur. #[track_caller] pub fn detach_and_log_err(self, cx: &App) { let location = core::panic::Location::caller(); @@ -143,53 +85,42 @@ where } } -impl Future for Task { +impl std::future::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)) => task.poll(cx), - } + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + // SAFETY: Task is a repr(transparent) wrapper around the scheduler Task, + // and we're just projecting the pin through to the inner task. + let inner = unsafe { self.map_unchecked_mut(|t| &mut t.0) }; + inner.poll(cx) } } -/// A task label is an opaque identifier that you can use to -/// refer to a task in tests. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] -pub struct TaskLabel(NonZeroUsize); +impl BackgroundExecutor { + /// Creates a new BackgroundExecutor from the given PlatformDispatcher. + pub fn new(dispatcher: Arc) -> Self { + #[cfg(any(test, feature = "test-support"))] + let scheduler: Arc = if let Some(test_dispatcher) = dispatcher.as_test() { + test_dispatcher.scheduler().clone() + } else { + Arc::new(PlatformScheduler::new(dispatcher.clone())) + }; -impl Default for TaskLabel { - fn default() -> Self { - Self::new() - } -} + #[cfg(not(any(test, feature = "test-support")))] + let scheduler: Arc = Arc::new(PlatformScheduler::new(dispatcher.clone())); -impl TaskLabel { - /// Construct a new task label. - pub fn new() -> Self { - static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1); - Self( - NEXT_TASK_LABEL - .fetch_add(1, Ordering::SeqCst) - .try_into() - .unwrap(), - ) + Self { + inner: crate::scheduler::BackgroundExecutor::new(scheduler), + dispatcher, + } } -} - -type AnyLocalFuture = Pin>>; - -type AnyFuture = Pin>>; -/// BackgroundExecutor lets you run things on background threads. -/// In production this is a thread pool with no ordering guarantees. -/// In tests this is simulated by running tasks one by one in a deterministic -/// (but arbitrary) order controlled by the `SEED` environment variable. -impl BackgroundExecutor { - #[doc(hidden)] - pub fn new(dispatcher: Arc) -> Self { - Self { dispatcher } + /// Returns the underlying crate::scheduler::BackgroundExecutor. + pub fn scheduler_executor(&self) -> crate::scheduler::BackgroundExecutor { + self.inner.clone() } /// Enqueues the given future to be run to completion on a background thread. @@ -198,10 +129,10 @@ impl BackgroundExecutor { where R: Send + 'static, { - self.spawn_with_priority(Priority::default(), future) + self.spawn_with_priority(Priority::default(), future.boxed()) } - /// Enqueues the given future to be run to completion on a background thread. + /// Enqueues the given future to be run to completion on a background thread with the given priority. #[track_caller] pub fn spawn_with_priority( &self, @@ -211,19 +142,21 @@ impl BackgroundExecutor { where R: Send + 'static, { - self.spawn_internal::(Box::pin(future), None, priority) + if priority == Priority::RealtimeAudio { + Task::from_scheduler(self.inner.spawn_realtime(future)) + } else { + Task::from_scheduler(self.inner.spawn_with_priority(priority, future)) + } } /// Enqueues the given future to be run to completion on a background thread and blocking the current task on it. - /// - /// This allows to spawn background work that borrows from its scope. Note that the supplied future will run to - /// completion before the current task is resumed, even if the current task is slated for cancellation. pub async fn await_on_background(&self, future: impl Future + Send) -> R where R: Send, { - // We need to ensure that cancellation of the parent task does not drop the environment - // before the our own task has completed or got cancelled. + use crate::RunnableMeta; + use parking_lot::{Condvar, Mutex}; + struct NotifyOnDrop<'a>(&'a (Condvar, Mutex)); impl Drop for NotifyOnDrop<'_> { @@ -259,11 +192,7 @@ impl BackgroundExecutor { future.await }, move |runnable| { - dispatcher.dispatch( - RunnableVariant::Meta(runnable), - None, - Priority::default(), - ) + dispatcher.dispatch(runnable, Priority::default()); }, ) }; @@ -271,240 +200,6 @@ impl BackgroundExecutor { task.await } - /// Enqueues the given future to be run to completion on a background thread. - /// The given label can be used to control the priority of the task in tests. - #[track_caller] - pub fn spawn_labeled( - &self, - label: TaskLabel, - future: impl Future + Send + 'static, - ) -> Task - where - R: Send + 'static, - { - self.spawn_internal::(Box::pin(future), Some(label), Priority::default()) - } - - #[track_caller] - fn spawn_internal( - &self, - future: AnyFuture, - label: Option, - priority: Priority, - ) -> Task { - let dispatcher = self.dispatcher.clone(); - let (runnable, task) = if let Priority::Realtime(realtime) = priority { - let location = core::panic::Location::caller(); - let (mut tx, rx) = flume::bounded::>(1); - - dispatcher.spawn_realtime( - realtime, - Box::new(move || { - while let Ok(runnable) = rx.recv() { - let start = Instant::now(); - let location = runnable.metadata().location; - let mut timing = TaskTiming { - location, - start, - end: None, - }; - profiler::add_task_timing(timing); - - runnable.run(); - - let end = Instant::now(); - timing.end = Some(end); - profiler::add_task_timing(timing); - } - }), - ); - - async_task::Builder::new() - .metadata(RunnableMeta { location }) - .spawn( - move |_| future, - move |runnable| { - let _ = tx.send(runnable); - }, - ) - } else { - let location = core::panic::Location::caller(); - async_task::Builder::new() - .metadata(RunnableMeta { location }) - .spawn( - move |_| future, - move |runnable| { - dispatcher.dispatch(RunnableVariant::Meta(runnable), label, priority) - }, - ) - }; - - runnable.schedule(); - Task(TaskState::Spawned(task)) - } - - /// Used by the test harness to run an async test in a synchronous fashion. - #[cfg(any(test, feature = "test-support"))] - #[track_caller] - pub fn block_test(&self, future: impl Future) -> R { - if let Ok(value) = self.block_internal(false, future, None) { - value - } else { - unreachable!() - } - } - - /// Block the current thread until the given future resolves. - /// Consider using `block_with_timeout` instead. - pub fn block(&self, future: impl Future) -> R { - if let Ok(value) = self.block_internal(true, future, None) { - value - } else { - unreachable!() - } - } - - #[cfg(not(any(test, feature = "test-support")))] - pub(crate) fn block_internal( - &self, - _background_only: bool, - future: Fut, - timeout: Option, - ) -> Result + use> { - use std::time::Instant; - - let mut future = Box::pin(future); - if timeout == Some(Duration::ZERO) { - return Err(future); - } - let deadline = timeout.map(|timeout| Instant::now() + timeout); - - let parker = parking::Parker::new(); - let unparker = parker.unparker(); - let waker = waker_fn(move || { - unparker.unpark(); - }); - let mut cx = std::task::Context::from_waker(&waker); - - loop { - match future.as_mut().poll(&mut cx) { - Poll::Ready(result) => return Ok(result), - Poll::Pending => { - let timeout = - deadline.map(|deadline| deadline.saturating_duration_since(Instant::now())); - if let Some(timeout) = timeout { - if !parker.park_timeout(timeout) - && deadline.is_some_and(|deadline| deadline < Instant::now()) - { - return Err(future); - } - } else { - parker.park(); - } - } - } - } - } - - #[cfg(any(test, feature = "test-support"))] - #[track_caller] - pub(crate) fn block_internal( - &self, - background_only: bool, - future: Fut, - timeout: Option, - ) -> Result + use> { - use std::sync::atomic::AtomicBool; - - use parking::Parker; - - let mut future = Box::pin(future); - if timeout == Some(Duration::ZERO) { - return Err(future); - } - let Some(dispatcher) = self.dispatcher.as_test() else { - return Err(future); - }; - - let mut max_ticks = if timeout.is_some() { - dispatcher.gen_block_on_ticks() - } else { - usize::MAX - }; - - let parker = Parker::new(); - let unparker = parker.unparker(); - - let awoken = Arc::new(AtomicBool::new(false)); - let waker = waker_fn({ - let awoken = awoken.clone(); - let unparker = unparker.clone(); - move || { - awoken.store(true, Ordering::SeqCst); - unparker.unpark(); - } - }); - let mut cx = std::task::Context::from_waker(&waker); - - let duration = Duration::from_secs( - option_env!("GPUI_TEST_TIMEOUT") - .and_then(|s| s.parse::().ok()) - .unwrap_or(180), - ); - let mut test_should_end_by = Instant::now() + duration; - - loop { - match future.as_mut().poll(&mut cx) { - Poll::Ready(result) => return Ok(result), - Poll::Pending => { - if max_ticks == 0 { - return Err(future); - } - max_ticks -= 1; - - if !dispatcher.tick(background_only) { - if awoken.swap(false, Ordering::SeqCst) { - continue; - } - - if !dispatcher.parking_allowed() { - if dispatcher.advance_clock_to_next_delayed() { - continue; - } - let mut backtrace_message = String::new(); - let mut waiting_message = String::new(); - if let Some(backtrace) = dispatcher.waiting_backtrace() { - backtrace_message = - format!("\nbacktrace of waiting future:\n{:?}", backtrace); - } - if let Some(waiting_hint) = dispatcher.waiting_hint() { - waiting_message = format!("\n waiting on: {}\n", waiting_hint); - } - panic!( - "parked with nothing left to run{waiting_message}{backtrace_message}", - ) - } - dispatcher.push_unparker(unparker.clone()); - parker.park_timeout(Duration::from_millis(1)); - if Instant::now() > test_should_end_by { - panic!("test timed out after {duration:?} with allow_parking") - } - } - } - } - } - } - - /// Block the current thread until the given future resolves - /// or `duration` has elapsed. - pub fn block_with_timeout( - &self, - duration: Duration, - future: Fut, - ) -> Result + use> { - self.block_internal(true, future, Some(duration)) - } - /// Scoped lets you start a number of tasks and waits /// for all of them to complete before returning. pub async fn scoped<'scope, F>(&self, scheduler: F) @@ -544,103 +239,116 @@ impl BackgroundExecutor { /// Calling this instead of `std::time::Instant::now` allows the use /// of fake timers in tests. pub fn now(&self) -> Instant { - self.dispatcher.now() + self.inner.scheduler().clock().now() } /// Returns a task that will complete after the given duration. /// Depending on other concurrent tasks the elapsed duration may be longer /// than requested. + #[track_caller] pub fn timer(&self, duration: Duration) -> Task<()> { if duration.is_zero() { return Task::ready(()); } - let location = core::panic::Location::caller(); - let (runnable, task) = async_task::Builder::new() - .metadata(RunnableMeta { location }) - .spawn(move |_| async move {}, { - let dispatcher = self.dispatcher.clone(); - move |runnable| dispatcher.dispatch_after(duration, RunnableVariant::Meta(runnable)) - }); - runnable.schedule(); - Task(TaskState::Spawned(task)) + self.spawn(self.inner.scheduler().timer(duration)) } - /// in tests, start_waiting lets you indicate which task is waiting (for debugging only) + /// In tests, run an arbitrary number of tasks (determined by the SEED environment variable) #[cfg(any(test, feature = "test-support"))] - pub fn start_waiting(&self) { - self.dispatcher.as_test().unwrap().start_waiting(); + pub fn simulate_random_delay(&self) -> impl Future + use<> { + self.dispatcher.as_test().unwrap().simulate_random_delay() } - /// in tests, removes the debugging data added by start_waiting + /// In tests, move time forward. This does not run any tasks, but does make `timer`s ready. #[cfg(any(test, feature = "test-support"))] - pub fn finish_waiting(&self) { - self.dispatcher.as_test().unwrap().finish_waiting(); + pub fn advance_clock(&self, duration: Duration) { + self.dispatcher.as_test().unwrap().advance_clock(duration) } - /// in tests, run an arbitrary number of tasks (determined by the SEED environment variable) + /// In tests, indicate which task is waiting (for debugging only). #[cfg(any(test, feature = "test-support"))] - pub fn simulate_random_delay(&self) -> impl Future + use<> { - self.dispatcher.as_test().unwrap().simulate_random_delay() + pub fn start_waiting(&self) { + self.dispatcher.as_test().unwrap().start_waiting(); } - /// in tests, indicate that a given task from `spawn_labeled` should run after everything else + /// In tests, removes the debugging data added by start_waiting. #[cfg(any(test, feature = "test-support"))] - pub fn deprioritize(&self, task_label: TaskLabel) { - self.dispatcher.as_test().unwrap().deprioritize(task_label) + pub fn finish_waiting(&self) { + self.dispatcher.as_test().unwrap().finish_waiting(); } - /// in tests, move time forward. This does not run any tasks, but does make `timer`s ready. + /// Adds detail to the "parked with nothing left to run" message. #[cfg(any(test, feature = "test-support"))] - pub fn advance_clock(&self, duration: Duration) { - self.dispatcher.as_test().unwrap().advance_clock(duration) + pub fn set_waiting_hint(&self, msg: Option) { + self.dispatcher.as_test().unwrap().set_waiting_hint(msg); } - /// in tests, run one task. + /// In tests, run one task. #[cfg(any(test, feature = "test-support"))] pub fn tick(&self) -> bool { - self.dispatcher.as_test().unwrap().tick(false) + self.dispatcher.as_test().unwrap().scheduler().tick() } - /// in tests, run all tasks that are ready to run. If after doing so - /// the test still has outstanding tasks, this will panic. (See also [`Self::allow_parking`]) + /// In tests, run tasks until the scheduler would park. #[cfg(any(test, feature = "test-support"))] pub fn run_until_parked(&self) { - self.dispatcher.as_test().unwrap().run_until_parked() + let scheduler = self.dispatcher.as_test().unwrap().scheduler(); + scheduler.run(); } - /// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks. - /// This is useful when you are integrating other (non-GPUI) futures, like disk access, that - /// do take real async time to run. + /// In tests, prevents `run_until_parked` from panicking if there are outstanding tasks. #[cfg(any(test, feature = "test-support"))] pub fn allow_parking(&self) { - self.dispatcher.as_test().unwrap().allow_parking(); + self.dispatcher + .as_test() + .unwrap() + .scheduler() + .allow_parking(); } - /// undoes the effect of [`Self::allow_parking`]. + /// Sets the range of ticks to run before timing out in block_on. #[cfg(any(test, feature = "test-support"))] - pub fn forbid_parking(&self) { - self.dispatcher.as_test().unwrap().forbid_parking(); + pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { + self.dispatcher + .as_test() + .unwrap() + .scheduler() + .set_timeout_ticks(range); } - /// adds detail to the "parked with nothing let to run" message. + /// Undoes the effect of [`Self::allow_parking`]. #[cfg(any(test, feature = "test-support"))] - pub fn set_waiting_hint(&self, msg: Option) { - self.dispatcher.as_test().unwrap().set_waiting_hint(msg); + pub fn forbid_parking(&self) { + self.dispatcher + .as_test() + .unwrap() + .scheduler() + .forbid_parking(); } - /// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable + /// In tests, returns the rng used by the dispatcher. #[cfg(any(test, feature = "test-support"))] - pub fn rng(&self) -> StdRng { - self.dispatcher.as_test().unwrap().rng() + pub fn rng(&self) -> crate::scheduler::SharedRng { + self.dispatcher.as_test().unwrap().scheduler().rng() } /// How many CPUs are available to the dispatcher. pub fn num_cpus(&self) -> usize { #[cfg(any(test, feature = "test-support"))] - return 4; + if let Some(test) = self.dispatcher.as_test() { + return test.num_cpus_override().unwrap_or(4); + } + num_cpus::get() + } - #[cfg(not(any(test, feature = "test-support")))] - return num_cpus::get(); + /// Override the number of CPUs reported by this executor in tests. + /// Panics if not called on a test executor. + #[cfg(any(test, feature = "test-support"))] + pub fn set_num_cpus(&self, count: usize) { + self.dispatcher + .as_test() + .expect("set_num_cpus can only be called on a test executor") + .set_num_cpus(count); } /// Whether we're on the main thread. @@ -648,132 +356,114 @@ impl BackgroundExecutor { self.dispatcher.is_main_thread() } - #[cfg(any(test, feature = "test-support"))] - /// in tests, control the number of ticks that `block_with_timeout` will run before timing out. - pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { - self.dispatcher.as_test().unwrap().set_block_on_ticks(range); + /// Wake the main event loop. + pub fn wake(&self) { + self.dispatcher.wakeup(); + } + + #[doc(hidden)] + pub fn dispatcher(&self) -> &Arc { + &self.dispatcher } } -/// ForegroundExecutor runs things on the main thread. impl ForegroundExecutor { /// Creates a new ForegroundExecutor from the given PlatformDispatcher. pub fn new(dispatcher: Arc) -> Self { + #[cfg(any(test, feature = "test-support"))] + let (scheduler, session_id): (Arc, _) = + if let Some(test_dispatcher) = dispatcher.as_test() { + ( + test_dispatcher.scheduler().clone(), + test_dispatcher.session_id(), + ) + } else { + let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); + let session_id = platform_scheduler.allocate_session_id(); + (platform_scheduler, session_id) + }; + + #[cfg(not(any(test, feature = "test-support")))] + let (scheduler, session_id): (Arc, _) = { + let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone())); + let session_id = platform_scheduler.allocate_session_id(); + (platform_scheduler, session_id) + }; + + let inner = crate::scheduler::ForegroundExecutor::new(session_id, scheduler); + Self { + inner, dispatcher, not_send: PhantomData, } } - /// Enqueues the given Task to run on the main thread at some point in the future. + /// Enqueues the given Task to run on the main thread. #[track_caller] pub fn spawn(&self, future: impl Future + 'static) -> Task where R: 'static, { - self.spawn_with_priority(Priority::default(), future) + Task::from_scheduler(self.inner.spawn(future.boxed_local())) } - /// Enqueues the given Task to run on the main thread at some point in the future. + /// Enqueues the given Task to run on the main thread with the given priority. #[track_caller] pub fn spawn_with_priority( &self, - priority: Priority, + _priority: Priority, future: impl Future + 'static, ) -> Task where R: 'static, { - let dispatcher = self.dispatcher.clone(); - let location = core::panic::Location::caller(); - - #[track_caller] - fn inner( - dispatcher: Arc, - future: AnyLocalFuture, - location: &'static core::panic::Location<'static>, - priority: Priority, - ) -> Task { - let (runnable, task) = spawn_local_with_source_location( - future, - move |runnable| { - dispatcher.dispatch_on_main_thread(RunnableVariant::Meta(runnable), priority) - }, - RunnableMeta { location }, - ); - runnable.schedule(); - Task(TaskState::Spawned(task)) - } - inner::(dispatcher, Box::pin(future), location, priority) + // Priority is ignored for foreground tasks - they run in order on the main thread + Task::from_scheduler(self.inner.spawn(future)) } -} -/// 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, - metadata: M, -) -> (Runnable, async_task::Task) -where - Fut: Future + 'static, - Fut::Output: 'static, - S: async_task::Schedule + Send + Sync + 'static, - M: '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()) - } + /// Used by the test harness to run an async test in a synchronous fashion. + #[cfg(any(test, feature = "test-support"))] + #[track_caller] + pub fn block_test(&self, future: impl Future) -> R { + use std::cell::Cell; - struct Checked { - id: ThreadId, - inner: ManuallyDrop, - location: &'static Location<'static>, - } + let scheduler = self.inner.scheduler(); - 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) }; - } + let output = Cell::new(None); + let future = async { + output.set(Some(future.await)); + }; + let mut future = std::pin::pin!(future); + + scheduler.block(None, future.as_mut(), None); + + output.take().expect("block_test future did not complete") } - impl Future for Checked { - type Output = F::Output; + /// Block the current thread until the given future resolves. + /// Consider using `block_with_timeout` instead. + pub fn block_on(&self, future: impl Future) -> R { + self.inner.block_on(future) + } - 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) } - } + /// Block the current thread until the given future resolves or the timeout elapses. + pub fn block_with_timeout>( + &self, + duration: Duration, + future: Fut, + ) -> Result + use> { + self.inner.block_with_timeout(duration, future) } - // 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(), - }; + #[doc(hidden)] + pub fn dispatcher(&self) -> &Arc { + &self.dispatcher + } - unsafe { - async_task::Builder::new() - .metadata(metadata) - .spawn_unchecked(move |_| future, schedule) + #[doc(hidden)] + pub fn scheduler_executor(&self) -> SchedulerForegroundExecutor { + self.inner.clone() } } @@ -834,6 +524,13 @@ impl Drop for Scope<'_> { // Wait until the channel is closed, which means that all of the spawned // futures have resolved. - self.executor.block(self.rx.next()); + let future = async { + self.rx.next().await; + }; + let mut future = std::pin::pin!(future); + self.executor + .inner + .scheduler() + .block(None, future.as_mut(), None); } } diff --git a/src/gpui.rs b/src/gpui.rs index 5120f7adfa..a52ef9fe0d 100644 --- a/src/gpui.rs +++ b/src/gpui.rs @@ -5,6 +5,7 @@ #![allow(unused_mut)] // False positives in platform specific code extern crate self as gpui; +extern crate gpui_ce_macros as gpui_macros; #[macro_use] mod action; @@ -20,6 +21,8 @@ pub mod default_colors; mod element; mod elements; mod executor; +mod platform_scheduler; +pub(crate) use platform_scheduler::PlatformScheduler; mod geometry; mod global; mod input; @@ -33,6 +36,9 @@ pub mod prelude; mod profiler; mod queue; mod scene; +/// The scheduler module provides task scheduling, execution, and timing primitives. +#[allow(missing_docs)] +pub mod scheduler; mod shared_string; mod shared_uri; mod style; @@ -81,7 +87,7 @@ pub use elements::*; pub use executor::*; pub use geometry::*; pub use global::*; -pub use gpui_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test}; +pub use gpui_ce_macros::{AppContext, IntoElement, Render, VisualContext, register_action, test}; pub use http_client; pub use input::*; pub use inspector::*; @@ -94,9 +100,14 @@ pub use profiler::*; pub(crate) use queue::{PriorityQueueReceiver, PriorityQueueSender}; pub use refineable::*; pub use scene::*; +// Re-export scheduler types selectively to avoid ambiguity with executor wrappers. +// Types like Task, BackgroundExecutor, ForegroundExecutor, FallibleTask, and Priority +// are re-exported via the executor module which wraps them. +pub use scheduler::{Clock, Instant, RunnableMeta, Scheduler, SessionId, Timer}; +#[cfg(any(test, feature = "test-support"))] +pub use scheduler::{SharedRng, TestScheduler, TestSchedulerConfig, Yield}; pub use shared_string::*; pub use shared_uri::*; -pub use smol::Timer; use std::{any::Any, future::Future}; pub use style::*; pub use styled::*; diff --git a/src/platform.rs b/src/platform.rs index faee76c9f0..2efd5a7ae9 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -12,9 +12,9 @@ use crate::{ Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds, DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, - Point, Priority, RealtimePriority, RenderGlyphParams, RenderImage, RenderImageParams, - RenderSvgParams, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, - SystemWindowTab, Task, TaskLabel, TaskTiming, ThreadTaskTimings, Window, WindowControlArea, + Point, Priority, RenderGlyphParams, RenderImage, RenderImageParams, + RenderSvgParams, RunnableMeta, Scene, ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, + SystemWindowTab, Task, TaskTiming, ThreadTaskTimings, Window, WindowControlArea, hash, point, px, size, }; use anyhow::Result; @@ -427,17 +427,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { /// This type is public so that our test macro can generate and use it, but it should not /// be considered part of our public API. #[doc(hidden)] -#[derive(Debug)] -pub struct RunnableMeta { - /// Location of the runnable - pub location: &'static core::panic::Location<'static>, -} - +/// A runnable task variant used by the platform dispatcher. #[doc(hidden)] -pub enum RunnableVariant { - Meta(Runnable), - Compat(Runnable), -} +pub type RunnableVariant = Runnable; /// This type is public so that our test macro can generate and use it, but it should not /// be considered part of our public API. @@ -446,15 +438,24 @@ pub trait PlatformDispatcher: Send + Sync { fn get_all_timings(&self) -> Vec; fn get_current_thread_timings(&self) -> Vec; fn is_main_thread(&self) -> bool; - fn dispatch(&self, runnable: RunnableVariant, label: Option, priority: Priority); + fn dispatch(&self, runnable: RunnableVariant, priority: Priority); fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority); fn dispatch_after(&self, duration: Duration, runnable: RunnableVariant); - fn spawn_realtime(&self, priority: RealtimePriority, f: Box); + fn spawn_realtime(&self, f: Box); + + /// Wake the main event loop. Used by external threads (e.g. a tokio watch + /// channel watcher) to signal that there is new data to render. The default + /// implementation is a no-op for platforms that don't need it. + fn wakeup(&self) {} fn now(&self) -> Instant { Instant::now() } + fn increase_timer_resolution(&self) -> util::Deferred> { + util::defer(Box::new(|| {})) + } + #[cfg(any(test, feature = "test-support"))] fn as_test(&self) -> Option<&TestDispatcher> { None @@ -777,13 +778,7 @@ pub(crate) struct PlatformInputHandler { handler: Box, } -#[cfg_attr( - all( - any(target_os = "linux", target_os = "freebsd"), - not(any(feature = "x11", feature = "wayland")) - ), - allow(dead_code) -)] +#[allow(dead_code)] impl PlatformInputHandler { pub fn new(cx: AsyncWindowContext, handler: Box) -> Self { Self { cx, handler } @@ -1062,6 +1057,7 @@ pub struct WindowOptions { /// The variables that can be configured when creating a new window #[derive(Debug)] +#[allow(dead_code)] pub(crate) struct WindowParams { pub bounds: Bounds, @@ -1760,6 +1756,7 @@ impl ClipboardString { .and_then(|m| serde_json::from_str(m).ok()) } + #[allow(dead_code)] pub(crate) fn text_hash(text: &str) -> u64 { let mut hasher = SeaHasher::new(); text.hash(&mut hasher); diff --git a/src/platform/cross/atlas.rs b/src/platform/cross/atlas.rs index 653ac3832e..293c8d0480 100644 --- a/src/platform/cross/atlas.rs +++ b/src/platform/cross/atlas.rs @@ -30,10 +30,6 @@ impl WgpuAtlas { self.0.lock().flush(encoder); } - pub fn after_frame(&self) { - // TODO(mdeand): Is this even necessary? - } - pub(crate) fn get_texture_info(&self, texture_id: AtlasTextureId) -> WgpuTextureInfo { let state = self.0.lock(); let texture = &state.storage[texture_id]; @@ -102,6 +98,7 @@ impl PlatformAtlas for WgpuAtlas { } } +#[allow(dead_code)] struct WgpuAtlasState { atlas_target: Option, atlas_target_view: Option, diff --git a/src/platform/cross/dispatcher.rs b/src/platform/cross/dispatcher.rs index c5689efc05..a1ead6e47b 100644 --- a/src/platform/cross/dispatcher.rs +++ b/src/platform/cross/dispatcher.rs @@ -1,5 +1,5 @@ use crate::{ - GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueSender, RealtimePriority, + GLOBAL_THREAD_TIMINGS, PlatformDispatcher, Priority, PriorityQueueSender, RunnableVariant, THREAD_TIMINGS, ThreadTaskTimings, }; use priority_threadpool::ThreadPool; @@ -58,16 +58,8 @@ impl PlatformDispatcher for Dispatcher { std::thread::current().id() == self.main_thread_id } - fn dispatch( - &self, - runnable: RunnableVariant, - _label: Option, - priority: Priority, - ) { - match runnable { - RunnableVariant::Meta(runnable) => self.threadpool.queue(&priority, runnable), - RunnableVariant::Compat(runnable) => self.threadpool.queue(&priority, runnable), - } + fn dispatch(&self, runnable: RunnableVariant, priority: Priority) { + self.threadpool.queue(&priority, runnable) } fn dispatch_on_main_thread(&self, runnable: RunnableVariant, priority: Priority) { @@ -82,19 +74,15 @@ impl PlatformDispatcher for Dispatcher { } fn dispatch_after(&self, duration: std::time::Duration, runnable: RunnableVariant) { - match runnable { - RunnableVariant::Meta(runnable) => { - self.threadpool - .queue_delayed(&Priority::Low, duration, runnable); - } - RunnableVariant::Compat(runnable) => { - self.threadpool - .queue_delayed(&Priority::Low, duration, runnable); - } - } + self.threadpool + .queue_delayed(&Priority::Low, duration, runnable); + } + + fn wakeup(&self) { + let _ = self.proxy.send_event(CrossEvent::WakeUp); } - fn spawn_realtime(&self, _priority: RealtimePriority, f: Box) { + fn spawn_realtime(&self, f: Box) { // TODO(mdeand): There's a crate (thread-priority) that implements thread // TODO(mdeand): priorities, but I don't want to add it right now. diff --git a/src/platform/cross/platform.rs b/src/platform/cross/platform.rs index c982db0cbd..e806799b12 100644 --- a/src/platform/cross/platform.rs +++ b/src/platform/cross/platform.rs @@ -72,7 +72,6 @@ impl CrossPlatform { let (main_tx, main_rx) = PriorityQueueReceiver::new(); let mut event_loop = winit::event_loop::EventLoop::::with_user_event().build()?; - event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); let event_loop_proxy = event_loop.create_proxy(); let dispatcher = Arc::new(Dispatcher::new(main_tx, event_loop_proxy.clone())); @@ -359,14 +358,7 @@ impl AppState { fn drain_main_queue(&mut self) { while let Ok(Some(runnable)) = self.main_rx.try_pop() { - match runnable { - RunnableVariant::Compat(runnable) => { - runnable.run(); - } - RunnableVariant::Meta(runnable) => { - runnable.run(); - } - } + runnable.run(); } } } @@ -407,9 +399,20 @@ impl winit::application::ApplicationHandler for AppState { self.drain_main_queue(); - for window in self.windows.values() { - window.window().request_redraw(); - } + // Do NOT unconditionally request_redraw() here. Rendering is driven + // by three sources: + // 1. CrossEvent::WakeUp — sent by BackgroundExecutor::wake() from + // external threads (e.g. a tokio watcher task) when new data + // arrives. The WakeUp handler calls request_redraw(). + // 2. CrossEvent::SurfacePresent — fired after each GPU present; the + // handler calls request_redraw() to chain the next frame when + // the window still has content to show. + // 3. OS events (resize, focus, etc.) which set dirty state via + // cx.notify(), causing the dispatcher to send WakeUp. + // + // With no unconditional redraw, the event loop genuinely sleeps when + // idle, dropping CPU usage to ~0%. + event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait); self.clear_active_context(); } @@ -531,10 +534,17 @@ impl winit::application::ApplicationHandler for AppState { |cb| { cb(crate::RequestFrameOptions { force_render: false, - require_presentation: true, + // Only present after an actual draw; don't keep the + // GPU busy re-presenting the same frame every tick. + require_presentation: false, }); }, ); + // Do NOT fall through to the unconditional request_redraw() at + // the end of window_event — RedrawRequested must not chain + // itself or the event loop never sleeps under ControlFlow::Wait. + self.clear_active_context(); + return; } winit::event::WindowEvent::KeyboardInput { @@ -727,6 +737,15 @@ impl winit::application::ApplicationHandler for AppState { _ => (), } + // Any window event may dirty the window via cx.notify(). + // Under ControlFlow::Wait, redraws only happen when explicitly + // requested, so we must request one here. The on_request_frame + // handler checks invalidator.is_dirty() before doing real work, + // so this is a no-op when nothing actually changed. + if let Some(window) = self.windows.get(&window_id) { + window.window().request_redraw(); + } + self.clear_active_context(); } } diff --git a/src/platform/cross/renderer.rs b/src/platform/cross/renderer.rs index 07a5c56903..d461ef344a 100644 --- a/src/platform/cross/renderer.rs +++ b/src/platform/cross/renderer.rs @@ -7,6 +7,7 @@ use crate::{ platform::cross::{atlas::WgpuAtlas, render_context::WgpuContext}, }; +#[allow(dead_code)] const fn map_attributes( attribs: &'static [wgpu::VertexAttribute; N], location_offset: u32, @@ -33,6 +34,7 @@ const fn map_attributes( } impl color::Hsla { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 4] = &[ wgpu::VertexAttribute { offset: std::mem::offset_of!(color::Hsla, h) as wgpu::BufferAddress, @@ -58,6 +60,7 @@ impl color::Hsla { } impl color::LinearColorStop { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 2] = &[ wgpu::VertexAttribute { offset: std::mem::offset_of!(LinearColorStop, color) as wgpu::BufferAddress, @@ -73,6 +76,7 @@ impl color::LinearColorStop { } impl color::Background { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 7] = &{ let linear_color_stop_vertex_attributes = map_attributes( LinearColorStop::VERTEX_ATTRIBUTES, @@ -122,6 +126,7 @@ struct GlobalParams { } impl GlobalParams { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 3] = &[ wgpu::VertexAttribute { offset: std::mem::offset_of!(GlobalParams, viewport_size) as wgpu::BufferAddress, @@ -149,6 +154,7 @@ struct Bounds { } impl geometry::Corners { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 4] = &[ wgpu::VertexAttribute { offset: std::mem::offset_of!(geometry::Corners, top_left) @@ -178,6 +184,7 @@ impl geometry::Corners { } impl geometry::Edges { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 4] = &[ wgpu::VertexAttribute { offset: std::mem::offset_of!(geometry::Edges, top) as wgpu::BufferAddress, @@ -206,6 +213,7 @@ impl geometry::Edges { } impl Bounds { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 2] = &[ wgpu::VertexAttribute { offset: std::mem::offset_of!(Bounds, origin) as wgpu::BufferAddress, @@ -228,6 +236,7 @@ struct SurfaceParams { } impl Quad { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 22] = &{ let bounds_vertex_attributes = map_attributes( Bounds::VERTEX_ATTRIBUTES, @@ -301,30 +310,36 @@ impl Quad { } #[repr(C)] +#[allow(dead_code)] struct QuadsData { globals: GlobalParams, } #[repr(C)] +#[allow(dead_code)] struct ShadowsData { globals: GlobalParams, } #[repr(C)] +#[allow(dead_code)] struct PathRasterizationData { globals: GlobalParams, } +#[allow(dead_code)] struct PathsData { globals: GlobalParams, t_sprite: wgpu::TextureView, s_sprite: wgpu::Sampler, } +#[allow(dead_code)] struct UnderlinesData { globals: GlobalParams, } +#[allow(dead_code)] struct MonoSpritesData { globals: GlobalParams, gamma_ratios: [f32; 4], @@ -333,12 +348,14 @@ struct MonoSpritesData { s_sprite: wgpu::Sampler, } +#[allow(dead_code)] struct PolySpritesData { globals: GlobalParams, t_sprite: wgpu::TextureView, s_sprite: wgpu::Sampler, } +#[allow(dead_code)] struct SurfacesData { globals: GlobalParams, surface_params: SurfaceParams, @@ -347,12 +364,14 @@ struct SurfacesData { s_texture: wgpu::Sampler, } +#[allow(dead_code)] struct PathSprite { bounds: geometry::Bounds, } #[repr(C)] #[derive(Clone, Copy)] +#[allow(dead_code)] struct PathRasterizationVertex { xy_position: geometry::Point, st_position: geometry::Point, @@ -361,6 +380,7 @@ struct PathRasterizationVertex { } impl PathRasterizationVertex { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 10] = &{ let color_vertex_attributes = map_attributes( color::Background::VERTEX_ATTRIBUTES, @@ -398,6 +418,7 @@ impl PathRasterizationVertex { ] }; + #[allow(dead_code)] fn layout() -> wgpu::VertexBufferLayout<'static> { wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, @@ -408,6 +429,7 @@ impl PathRasterizationVertex { } impl AtlasTextureId { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 2] = &{ [ wgpu::VertexAttribute { @@ -425,12 +447,14 @@ impl AtlasTextureId { } #[repr(C)] +#[allow(dead_code)] struct AtlasBounds { origin: [i32; 2], size: [i32; 2], } impl AtlasBounds { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 2] = &{ [ wgpu::VertexAttribute { @@ -448,6 +472,7 @@ impl AtlasBounds { } impl AtlasTile { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 6] = &{ let texture_id_vertex_attributes = map_attributes( AtlasTextureId::VERTEX_ATTRIBUTES, @@ -481,6 +506,7 @@ impl AtlasTile { } impl TransformationMatrix { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 2] = &{ [ wgpu::VertexAttribute { @@ -500,6 +526,7 @@ impl TransformationMatrix { } impl MonochromeSprite { + #[allow(dead_code)] const VERTEX_ATTRIBUTES: &'static [wgpu::VertexAttribute; 16] = &{ let bounds_vertex_attributes = map_attributes( Bounds::VERTEX_ATTRIBUTES, @@ -569,6 +596,7 @@ struct ColorAdjustments { } struct WgpuPipelines { + #[allow(dead_code)] color_targets: Vec>, quads_bind_group_layout: wgpu::BindGroupLayout, @@ -1114,6 +1142,7 @@ impl WgpuPipelines { } struct RenderingParameters { + #[allow(dead_code)] path_sample_count: u32, gamma_ratios: [f32; 4], grayscale_enhanced_contrast: f32, @@ -1593,8 +1622,8 @@ impl WgpuRenderer { } PrimitiveBatch::Surfaces(surfaces) => { for surface in surfaces { - if let crate::SurfaceContent::Wgpu(surface_id) = &surface.content { - if let Some(idx) = + let crate::SurfaceContent::Wgpu(surface_id) = &surface.content; + if let Some(idx) = self.context.surface_registry.front_index(*surface_id) { if self @@ -1709,7 +1738,6 @@ impl WgpuRenderer { seen_surfaces.push(*surface_id); } } - } } } // TODO(mdeand): Implement paths rendering. @@ -1735,10 +1763,12 @@ impl WgpuRenderer { .configure(&self.context.device, &self.surface_configuration); } + #[allow(dead_code)] pub fn sprite_atlas(&self) -> Arc { self.atlas.clone() } + #[allow(dead_code)] pub fn gpu_specs(&self) -> GpuSpecs { let info = self.context.adapter.get_info(); GpuSpecs { @@ -1749,6 +1779,7 @@ impl WgpuRenderer { } } + #[allow(dead_code)] pub fn update_transparency(&mut self, transparent: bool) { self.surface_configuration.alpha_mode = if transparent { wgpu::CompositeAlphaMode::PreMultiplied @@ -1761,6 +1792,7 @@ impl WgpuRenderer { .configure(&self.context.device, &self.surface_configuration); } + #[allow(dead_code)] pub fn viewport_size(&self) -> geometry::Size { geometry::Size { width: DevicePixels(self.surface_configuration.width as i32), diff --git a/src/platform/cross/surface_registry.rs b/src/platform/cross/surface_registry.rs index 4123ab90ac..c4ef6da22d 100644 --- a/src/platform/cross/surface_registry.rs +++ b/src/platform/cross/surface_registry.rs @@ -75,6 +75,7 @@ impl SurfaceRegistry { } /// Get the front buffer's `TextureView` (what the renderer reads from). + #[allow(dead_code)] pub fn front_view(&self, id: SurfaceId) -> Option { // clone an already-created view instead of making a new one every frame. let surfaces = self.surfaces.lock().unwrap(); diff --git a/src/platform/test/dispatcher.rs b/src/platform/test/dispatcher.rs index c271430586..c13913779e 100644 --- a/src/platform/test/dispatcher.rs +++ b/src/platform/test/dispatcher.rs @@ -1,6 +1,7 @@ -use crate::{PlatformDispatcher, Priority, RunnableVariant, TaskLabel}; +use crate::scheduler::{Instant, SessionId, TestScheduler, TestSchedulerConfig}; +use crate::{PlatformDispatcher, Priority, RunnableVariant}; use backtrace::Backtrace; -use collections::{HashMap, HashSet, VecDeque}; +use collections::{HashMap, VecDeque}; use parking::Unparker; use parking_lot::Mutex; use rand::prelude::*; @@ -10,7 +11,7 @@ use std::{ pin::Pin, sync::Arc, task::{Context, Poll}, - time::{Duration, Instant}, + time::Duration, }; use util::post_inc; @@ -21,13 +22,15 @@ struct TestDispatcherId(usize); pub struct TestDispatcher { id: TestDispatcherId, state: Arc>, + test_scheduler: Arc, + session_id: SessionId, + num_cpus: Arc>>, } struct TestDispatcherState { random: StdRng, foreground: HashMap>, background: Vec, - deprioritized_background: Vec, delayed: Vec<(Duration, RunnableVariant)>, start_time: Instant, time: Duration, @@ -36,18 +39,20 @@ struct TestDispatcherState { allow_parking: bool, waiting_hint: Option, waiting_backtrace: Option, - deprioritized_task_labels: HashSet, block_on_ticks: RangeInclusive, unparkers: Vec, } impl TestDispatcher { - pub fn new(random: StdRng) -> Self { + pub fn new(seed: u64) -> Self { + let random = StdRng::seed_from_u64(seed); + let test_scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(seed))); + let session_id = test_scheduler.allocate_session_id(); + let state = TestDispatcherState { random, foreground: HashMap::default(), background: Vec::new(), - deprioritized_background: Vec::new(), delayed: Vec::new(), time: Duration::ZERO, start_time: Instant::now(), @@ -56,7 +61,6 @@ impl TestDispatcher { allow_parking: false, waiting_hint: None, waiting_backtrace: None, - deprioritized_task_labels: Default::default(), block_on_ticks: 0..=1000, unparkers: Default::default(), }; @@ -64,9 +68,32 @@ impl TestDispatcher { TestDispatcher { id: TestDispatcherId(0), state: Arc::new(Mutex::new(state)), + test_scheduler, + session_id, + num_cpus: Arc::new(Mutex::new(None)), } } + /// Returns the TestScheduler backing this dispatcher. + pub fn scheduler(&self) -> &Arc { + &self.test_scheduler + } + + /// Returns the session ID for this dispatcher. + pub fn session_id(&self) -> SessionId { + self.session_id + } + + /// Returns the number of CPUs override, if set. + pub fn num_cpus_override(&self) -> Option { + *self.num_cpus.lock() + } + + /// Sets the number of CPUs override for testing. + pub fn set_num_cpus(&self, count: usize) { + *self.num_cpus.lock() = Some(count); + } + pub fn advance_clock(&self, by: Duration) { let new_now = self.state.lock().time + by; loop { @@ -143,13 +170,7 @@ impl TestDispatcher { let runnable; let main_thread; if foreground_len == 0 && background_len == 0 { - let deprioritized_background_len = state.deprioritized_background.len(); - if deprioritized_background_len == 0 { - return false; - } - let ix = state.random.random_range(0..deprioritized_background_len); - main_thread = false; - runnable = state.deprioritized_background.swap_remove(ix); + return false; } else { main_thread = state.random.random_ratio( foreground_len as u32, @@ -176,21 +197,15 @@ impl TestDispatcher { drop(state); // todo(localcc): add timings to tests - match runnable { - RunnableVariant::Meta(runnable) => runnable.run(), - RunnableVariant::Compat(runnable) => runnable.run(), - }; + runnable.run(); self.state.lock().is_main_thread = was_main_thread; true } - pub fn deprioritize(&self, task_label: TaskLabel) { - self.state - .lock() - .deprioritized_task_labels - .insert(task_label); + pub fn drain_tasks(&self) { + self.test_scheduler.drain_tasks(); } pub fn run_until_parked(&self) { @@ -233,7 +248,7 @@ impl TestDispatcher { } pub fn rng(&self) -> StdRng { - self.state.lock().random.clone() + StdRng::from_rng(&mut self.state.lock().random) } pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { @@ -262,6 +277,9 @@ impl Clone for TestDispatcher { Self { id: TestDispatcherId(id), state: self.state.clone(), + test_scheduler: self.test_scheduler.clone(), + session_id: self.session_id, + num_cpus: self.num_cpus.clone(), } } } @@ -284,15 +302,8 @@ impl PlatformDispatcher for TestDispatcher { state.start_time + state.time } - fn dispatch(&self, runnable: RunnableVariant, label: Option, _priority: Priority) { - { - let mut state = self.state.lock(); - if label.is_some_and(|label| state.deprioritized_task_labels.contains(&label)) { - state.deprioritized_background.push(runnable); - } else { - state.background.push(runnable); - } - } + fn dispatch(&self, runnable: RunnableVariant, _priority: Priority) { + self.state.lock().background.push(runnable); self.unpark_all(); } @@ -319,7 +330,7 @@ impl PlatformDispatcher for TestDispatcher { Some(self) } - fn spawn_realtime(&self, _priority: crate::RealtimePriority, f: Box) { + fn spawn_realtime(&self, f: Box) { std::thread::spawn(move || { f(); }); diff --git a/src/platform_scheduler.rs b/src/platform_scheduler.rs new file mode 100644 index 0000000000..0e349b34ab --- /dev/null +++ b/src/platform_scheduler.rs @@ -0,0 +1,155 @@ +use crate::scheduler::Instant; +#[cfg(any(test, feature = "test-support"))] +use crate::scheduler::TestScheduler; +use crate::scheduler::{Clock, Priority, Scheduler, SessionId, Timer}; +use crate::{PlatformDispatcher, RunnableMeta}; +use async_task::Runnable; +use chrono::{DateTime, Utc}; +use futures::channel::oneshot; +#[cfg(not(target_family = "wasm"))] +use std::task::{Context, Poll}; +use std::{ + future::Future, + pin::Pin, + sync::{ + Arc, + atomic::{AtomicU16, Ordering}, + }, + time::Duration, +}; + +/// A production implementation of [`Scheduler`] that wraps a [`PlatformDispatcher`]. +/// +/// This allows GPUI to use the scheduler crate's executor types with the platform's +/// native dispatch mechanisms (e.g., Grand Central Dispatch on macOS). +pub struct PlatformScheduler { + dispatcher: Arc, + clock: Arc, + next_session_id: AtomicU16, +} + +impl PlatformScheduler { + pub fn new(dispatcher: Arc) -> Self { + Self { + dispatcher: dispatcher.clone(), + clock: Arc::new(PlatformClock { dispatcher }), + next_session_id: AtomicU16::new(0), + } + } + + pub fn allocate_session_id(&self) -> SessionId { + SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst)) + } +} + +impl Scheduler for PlatformScheduler { + fn block( + &self, + _session_id: Option, + #[cfg_attr(target_family = "wasm", allow(unused_mut))] mut future: Pin< + &mut dyn Future, + >, + #[cfg_attr(target_family = "wasm", allow(unused_variables))] timeout: Option, + ) -> bool { + #[cfg(target_family = "wasm")] + { + let _ = (&future, &timeout); + panic!("Cannot block on wasm") + } + #[cfg(not(target_family = "wasm"))] + { + use waker_fn::waker_fn; + let deadline = timeout.map(|t| Instant::now() + t); + let parker = parking::Parker::new(); + let unparker = parker.unparker(); + let waker = waker_fn(move || { + unparker.unpark(); + }); + let mut cx = Context::from_waker(&waker); + if let Poll::Ready(()) = future.as_mut().poll(&mut cx) { + return true; + } + + let park_deadline = |deadline: Instant| { + let _timer_guard = self.dispatcher.increase_timer_resolution(); + parker.park_deadline(deadline) + }; + + loop { + match deadline { + Some(deadline) if !park_deadline(deadline) && deadline <= Instant::now() => { + return false; + } + Some(_) => (), + None => parker.park(), + } + if let Poll::Ready(()) = future.as_mut().poll(&mut cx) { + break true; + } + } + } + } + + fn schedule_foreground(&self, _session_id: SessionId, runnable: Runnable) { + self.dispatcher + .dispatch_on_main_thread(runnable, Priority::default()); + } + + fn schedule_background_with_priority( + &self, + runnable: Runnable, + priority: Priority, + ) { + self.dispatcher.dispatch(runnable, priority); + } + + fn spawn_realtime(&self, f: Box) { + self.dispatcher.spawn_realtime(f); + } + + #[track_caller] + fn timer(&self, duration: Duration) -> Timer { + let (tx, rx) = oneshot::channel(); + let dispatcher = self.dispatcher.clone(); + + // Create a runnable that will send the completion signal + let location = std::panic::Location::caller(); + let (runnable, _task) = async_task::Builder::new() + .metadata(RunnableMeta { location }) + .spawn( + move |_| async move { + let _ = tx.send(()); + }, + move |runnable| { + dispatcher.dispatch_after(duration, runnable); + }, + ); + runnable.schedule(); + + Timer::new(rx) + } + + fn clock(&self) -> Arc { + self.clock.clone() + } + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&self) -> Option<&TestScheduler> { + None + } +} + +/// A production clock that uses the platform dispatcher's time. +struct PlatformClock { + dispatcher: Arc, +} + +impl Clock for PlatformClock { + fn utc_now(&self) -> DateTime { + Utc::now() + } + + fn now(&self) -> Instant { + self.dispatcher.now() + } +} diff --git a/src/profiler.rs b/src/profiler.rs index 73f435d7e7..841a245325 100644 --- a/src/profiler.rs +++ b/src/profiler.rs @@ -217,6 +217,7 @@ impl Drop for ThreadTimings { } } +#[allow(dead_code)] pub(crate) fn add_task_timing(timing: TaskTiming) { THREAD_TIMINGS.with(|timings| { let mut timings = timings.lock(); diff --git a/src/queue.rs b/src/queue.rs index 7fa800a4e0..c3978e4e6b 100644 --- a/src/queue.rs +++ b/src/queue.rs @@ -39,7 +39,7 @@ impl PriorityQueueState { let mut queues = self.queues.lock(); match priority { - Priority::Realtime(_) => unreachable!(), + Priority::RealtimeAudio => unreachable!(), Priority::High => queues.high_priority.push(item), Priority::Medium => queues.medium_priority.push(item), Priority::Low => queues.low_priority.push(item), @@ -217,29 +217,29 @@ impl PriorityQueueReceiver { self.state.recv()? }; - let high = P::High.probability() * !queues.high_priority.is_empty() as u32; - let medium = P::Medium.probability() * !queues.medium_priority.is_empty() as u32; - let low = P::Low.probability() * !queues.low_priority.is_empty() as u32; + let high = P::High.weight() * !queues.high_priority.is_empty() as u32; + let medium = P::Medium.weight() * !queues.medium_priority.is_empty() as u32; + let low = P::Low.weight() * !queues.low_priority.is_empty() as u32; let mut mass = high + medium + low; //% if !queues.high_priority.is_empty() { - let flip = self.rand.random_ratio(P::High.probability(), mass); + let flip = self.rand.random_ratio(P::High.weight(), mass); if flip { return Ok(queues.high_priority.pop()); } - mass -= P::High.probability(); + mass -= P::High.weight(); } if !queues.medium_priority.is_empty() { - let flip = self.rand.random_ratio(P::Medium.probability(), mass); + let flip = self.rand.random_ratio(P::Medium.weight(), mass); if flip { return Ok(queues.medium_priority.pop()); } - mass -= P::Medium.probability(); + mass -= P::Medium.weight(); } if !queues.low_priority.is_empty() { - let flip = self.rand.random_ratio(P::Low.probability(), mass); + let flip = self.rand.random_ratio(P::Low.weight(), mass); if flip { return Ok(queues.low_priority.pop()); } diff --git a/src/scene.rs b/src/scene.rs index 85ac8eabfa..2607c2e45a 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -402,6 +402,7 @@ impl<'a> Iterator for BatchIterator<'a> { } #[derive(Debug)] +#[allow(dead_code)] pub(crate) enum PrimitiveBatch<'a> { Shadows(&'a [Shadow]), Quads(&'a [Quad]), diff --git a/src/scheduler/clock.rs b/src/scheduler/clock.rs new file mode 100644 index 0000000000..8437ab1cd7 --- /dev/null +++ b/src/scheduler/clock.rs @@ -0,0 +1,57 @@ +use chrono::{DateTime, Utc}; +use parking_lot::Mutex; +use std::time::Duration; + +pub use web_time::Instant; + +/// Interface for providing current time and monotonic instants. +pub trait Clock { + /// Returns the current UTC date and time. + fn utc_now(&self) -> DateTime; + + /// Returns the current monotonic instant. + fn now(&self) -> Instant; +} + +/// A mock clock implementation for use in tests. +pub struct TestClock(Mutex); + +struct TestClockState { + now: Instant, + utc_now: DateTime, +} + +impl TestClock { + /// Creates a new TestClock initialized to a fixed start time. + 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, + })) + } + + /// Sets the current UTC time for the clock. + pub fn set_utc_now(&self, now: DateTime) { + let mut state = self.0.lock(); + state.utc_now = now; + } + + /// Advances the clock by the given duration. + 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/src/scheduler/executor.rs b/src/scheduler/executor.rs new file mode 100644 index 0000000000..c973f1ab53 --- /dev/null +++ b/src/scheduler/executor.rs @@ -0,0 +1,396 @@ +use super::{Instant, Priority, RunnableMeta, Scheduler, SessionId, Timer}; +use std::{ + future::Future, + marker::PhantomData, + mem::ManuallyDrop, + panic::Location, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, + thread::{self, ThreadId}, + time::Duration, +}; + +/// An executor for running tasks on the foreground (main/UI) thread. +#[derive(Clone)] +pub struct ForegroundExecutor { + session_id: SessionId, + scheduler: Arc, + not_send: PhantomData>, +} + +impl ForegroundExecutor { + /// Creates a new ForegroundExecutor for the given session. + pub fn new(session_id: SessionId, scheduler: Arc) -> Self { + Self { + session_id, + scheduler, + not_send: PhantomData, + } + } + + /// Returns the session ID associated with this executor. + pub fn session_id(&self) -> SessionId { + self.session_id + } + + /// Returns the underlying scheduler. + pub fn scheduler(&self) -> &Arc { + &self.scheduler + } + + /// Spawns a task to run on the foreground thread. + #[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 location = Location::caller(); + let (runnable, task) = spawn_local_with_source_location( + future, + move |runnable| { + scheduler.schedule_foreground(session_id, runnable); + }, + RunnableMeta { location }, + ); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + + /// Blocks the current thread until the given future completes. + pub fn block_on(&self, future: Fut) -> Fut::Output { + use std::cell::Cell; + + let output = Cell::new(None); + + let future = async { + output.set(Some(future.await)); + }; + let mut future = std::pin::pin!(future); + + self.scheduler + .block(Some(self.session_id), future.as_mut(), None); + + output.take().expect("block_on future did not complete") + } + + /// Block until the future completes or timeout occurs. + /// Returns Ok(output) if completed, Err(future) if timed out. + pub fn block_with_timeout( + &self, + timeout: Duration, + future: Fut, + ) -> Result + use> { + use std::cell::Cell; + + let output = Cell::new(None); + let mut future = Box::pin(future); + + { + let future_ref = &mut future; + let wrapper = async { + output.set(Some(future_ref.await)); + }; + let mut wrapper = std::pin::pin!(wrapper); + + self.scheduler + .block(Some(self.session_id), wrapper.as_mut(), Some(timeout)); + } + + match output.take() { + Some(value) => Ok(value), + None => Err(future), + } + } + + /// Creates a timer that resolves after the given duration. + #[track_caller] + pub fn timer(&self, duration: Duration) -> Timer { + self.scheduler.timer(duration) + } + + /// Returns the current monotonic instant from the scheduler's clock. + pub fn now(&self) -> Instant { + self.scheduler.clock().now() + } +} + +/// An executor for running tasks in the background. +#[derive(Clone)] +pub struct BackgroundExecutor { + scheduler: Arc, +} + +impl BackgroundExecutor { + /// Creates a new BackgroundExecutor. + pub fn new(scheduler: Arc) -> Self { + Self { scheduler } + } + + /// Spawns a background task with default priority. + #[track_caller] + pub fn spawn(&self, future: F) -> Task + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + self.spawn_with_priority(Priority::default(), future) + } + + /// Spawns a background task with the given priority. + #[track_caller] + pub fn spawn_with_priority(&self, priority: Priority, future: F) -> Task + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let scheduler = Arc::clone(&self.scheduler); + let location = Location::caller(); + let (runnable, task) = async_task::Builder::new() + .metadata(RunnableMeta { location }) + .spawn( + move |_| future, + move |runnable| { + scheduler.schedule_background_with_priority(runnable, priority); + }, + ); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + + /// Spawns a future on a dedicated realtime thread for audio processing. + #[track_caller] + pub fn spawn_realtime(&self, future: F) -> Task + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let location = Location::caller(); + let (tx, rx) = flume::bounded::>(1); + + self.scheduler.spawn_realtime(Box::new(move || { + while let Ok(runnable) = rx.recv() { + runnable.run(); + } + })); + + let (runnable, task) = async_task::Builder::new() + .metadata(RunnableMeta { location }) + .spawn( + move |_| future, + move |runnable| { + let _ = tx.send(runnable); + }, + ); + runnable.schedule(); + Task(TaskState::Spawned(task)) + } + + /// Creates a timer that resolves after the given duration. + #[track_caller] + pub fn timer(&self, duration: Duration) -> Timer { + self.scheduler.timer(duration) + } + + /// Returns the current monotonic instant from the scheduler's clock. + pub fn now(&self) -> Instant { + self.scheduler.clock().now() + } + + /// Returns the underlying scheduler. + 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))) + } + + /// Creates a Task from an async_task::Task + pub fn from_async_task(task: async_task::Task) -> Self { + Task(TaskState::Spawned(task)) + } + + /// Returns true if the task has completed and its value is ready. + 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(), + } + } + + /// Converts this task into a fallible task that returns `Option`. + pub fn fallible(self) -> FallibleTask { + FallibleTask(match self.0 { + TaskState::Ready(val) => FallibleTaskState::Ready(val), + TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()), + }) + } +} + +/// A task that returns `Option` instead of panicking when cancelled. +#[must_use] +pub struct FallibleTask(FallibleTaskState); + +enum FallibleTaskState { + /// A task that is ready to return a value + Ready(Option), + + /// A task that is currently running (wraps async_task::FallibleTask). + Spawned(async_task::FallibleTask), +} + +impl FallibleTask { + /// Creates a new fallible task that will resolve with the value. + pub fn ready(val: T) -> Self { + FallibleTask(FallibleTaskState::Ready(Some(val))) + } + + /// Detaching a task runs it to completion in the background. + pub fn detach(self) { + match self.0 { + FallibleTaskState::Ready(_) => {} + FallibleTaskState::Spawned(task) => task.detach(), + } + } +} + +impl Future for FallibleTask { + type Output = Option; + + fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll { + match unsafe { self.get_unchecked_mut() } { + FallibleTask(FallibleTaskState::Ready(val)) => Poll::Ready(val.take()), + FallibleTask(FallibleTaskState::Spawned(task)) => Pin::new(task).poll(cx), + } + } +} + +impl std::fmt::Debug for FallibleTask { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self.0 { + FallibleTaskState::Ready(_) => f.debug_tuple("FallibleTask::Ready").finish(), + FallibleTaskState::Spawned(task) => { + f.debug_tuple("FallibleTask::Spawned").field(task).finish() + } + } + } +} + +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. +#[track_caller] +fn spawn_local_with_source_location( + future: Fut, + schedule: S, + metadata: RunnableMeta, +) -> ( + 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_eq!( + 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) } + } + } + + let location = metadata.location; + + unsafe { + async_task::Builder::new() + .metadata(metadata) + .spawn_unchecked( + move |_| Checked { + id: thread_id(), + inner: ManuallyDrop::new(future), + location, + }, + schedule, + ) + } +} diff --git a/src/scheduler/mod.rs b/src/scheduler/mod.rs new file mode 100644 index 0000000000..53bb2eda66 --- /dev/null +++ b/src/scheduler/mod.rs @@ -0,0 +1,149 @@ +mod clock; +mod executor; +#[cfg(any(test, feature = "test-support"))] +mod test_scheduler; +#[cfg(test)] +mod tests; + +pub use clock::*; +pub use executor::*; +#[cfg(any(test, feature = "test-support"))] +pub use test_scheduler::*; + +use async_task::Runnable; +use futures::channel::oneshot; +use std::{ + future::Future, + panic::Location, + pin::Pin, + sync::Arc, + task::{Context, Poll}, + time::Duration, +}; + +/// Task priority for background tasks. +/// +/// Higher priority tasks are more likely to be scheduled before lower priority tasks, +/// but this is not a strict guarantee - the scheduler may interleave tasks of different +/// priorities to prevent starvation. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[repr(u8)] +pub enum Priority { + /// Realtime priority + /// + /// Spawning a task with this priority will spin it off on a separate thread dedicated just to that task. Only use for audio. + RealtimeAudio, + /// High priority - use for tasks critical to user experience/responsiveness. + High, + /// Medium priority - suitable for most use cases. + #[default] + Medium, + /// Low priority - use for background work that can be deprioritized. + Low, +} + +impl Priority { + /// Returns the relative probability weight for this priority level. + /// Used by schedulers to determine task selection probability. + pub const fn weight(self) -> u32 { + match self { + Priority::High => 60, + Priority::Medium => 30, + Priority::Low => 10, + // realtime priorities are not considered for probability scheduling + Priority::RealtimeAudio => 0, + } + } +} + +/// Metadata attached to runnables for debugging and profiling. +#[derive(Clone)] +pub struct RunnableMeta { + /// The source location where the task was spawned. + pub location: &'static Location<'static>, +} + +impl std::fmt::Debug for RunnableMeta { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RunnableMeta") + .field("location", &self.location) + .finish() + } +} + +/// Interface for scheduling tasks on different threads or execution contexts. +pub trait Scheduler: Send + Sync { + /// Block until the given future completes or timeout occurs. + /// + /// Returns `true` if the future completed, `false` if it timed out. + /// The future is passed as a pinned mutable reference so the caller + /// retains ownership and can continue polling or return it on timeout. + fn block( + &self, + session_id: Option, + future: Pin<&mut dyn Future>, + timeout: Option, + ) -> bool; + + /// Schedules a task to run on the foreground (main/UI) thread. + fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable); + + /// Schedule a background task with the given priority. + fn schedule_background_with_priority( + &self, + runnable: Runnable, + priority: Priority, + ); + + /// Spawn a closure on a dedicated realtime thread for audio processing. + fn spawn_realtime(&self, f: Box); + + /// Schedule a background task with default (medium) priority. + fn schedule_background(&self, runnable: Runnable) { + self.schedule_background_with_priority(runnable, Priority::default()); + } + + /// Creates a timer that resolves after the given duration. + #[track_caller] + fn timer(&self, timeout: Duration) -> Timer; + + /// Returns the clock used by this scheduler. + fn clock(&self) -> Arc; + + #[cfg(any(test, feature = "test-support"))] + fn as_test(&self) -> Option<&TestScheduler> { + None + } +} + +/// A unique identifier for a window or application session. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub struct SessionId(u16); + +impl SessionId { + /// Creates a new SessionId from the given u16. + pub fn new(id: u16) -> Self { + SessionId(id) + } +} + +/// A future that resolves after a period of time. +pub struct Timer(oneshot::Receiver<()>); + +impl Timer { + /// Creates a new timer from a oneshot receiver. + 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 Pin::new(&mut self.0).poll(cx) { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/src/scheduler/test_scheduler.rs b/src/scheduler/test_scheduler.rs new file mode 100644 index 0000000000..e3f4400a7a --- /dev/null +++ b/src/scheduler/test_scheduler.rs @@ -0,0 +1,885 @@ +use super::{ + BackgroundExecutor, Clock, ForegroundExecutor, Instant, Priority, RunnableMeta, Scheduler, + SessionId, TestClock, Timer, +}; +use async_task::Runnable; +use backtrace::{Backtrace, BacktraceFrame}; +use futures::channel::oneshot; +use parking_lot::{Mutex, MutexGuard}; +use rand::{ + distr::{StandardUniform, uniform::SampleRange, uniform::SampleUniform}, + prelude::*, +}; +use std::{ + any::type_name_of_val, + collections::{BTreeMap, HashSet, 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, +}; + +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..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), + is_main_thread: true, + non_determinism_error: None, + finished: false, + parking_allowed_once: false, + unparked: false, + })), + clock: Arc::new(TestClock::new()), + thread: thread::current(), + } + } + + pub fn end_test(&self) { + let mut state = self.state.lock(); + if let Some((message, backtrace)) = &state.non_determinism_error { + panic!("{}\n{:?}", message, backtrace) + } + state.finished = true; + } + + pub fn clock(&self) -> Arc { + self.clock.clone() + } + + pub fn rng(&self) -> SharedRng { + SharedRng(self.rng.clone()) + } + + pub fn set_timeout_ticks(&self, timeout_ticks: RangeInclusive) { + self.state.lock().timeout_ticks = timeout_ticks; + } + + pub fn allow_parking(&self) { + let mut state = self.state.lock(); + state.allow_parking = true; + state.parking_allowed_once = true; + } + + pub fn forbid_parking(&self) { + self.state.lock().allow_parking = false; + } + + pub fn parking_allowed(&self) -> bool { + self.state.lock().allow_parking + } + + pub fn is_main_thread(&self) -> bool { + self.state.lock().is_main_thread + } + + /// Allocate a new session ID for foreground task scheduling. + /// This is used by GPUI's TestDispatcher to map dispatcher instances to sessions. + pub fn allocate_session_id(&self) -> SessionId { + let mut state = self.state.lock(); + state.next_session_id.0 += 1; + state.next_session_id + } + + /// Create a foreground executor for this scheduler + pub fn foreground(self: &Arc) -> ForegroundExecutor { + let session_id = self.allocate_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 + } + } + + /// Execute one tick of the scheduler, processing expired timers and running + /// at most one task. Returns true if any work was done. + /// + /// This is the public interface for GPUI's TestDispatcher to drive task execution. + pub fn tick(&self) -> bool { + self.step_filtered(false) + } + + /// Execute one tick, but only run background tasks (no foreground/session tasks). + /// Returns true if any work was done. + pub fn tick_background_only(&self) -> bool { + self.step_filtered(true) + } + + /// Check if there are any pending tasks or timers that could run. + pub fn has_pending_tasks(&self) -> bool { + let state = self.state.lock(); + !state.runnables.is_empty() || !state.timers.is_empty() + } + + /// Returns counts of (foreground_tasks, background_tasks) currently queued. + /// Foreground tasks are those with a session_id, background tasks have none. + pub fn pending_task_counts(&self) -> (usize, usize) { + let state = self.state.lock(); + let foreground = state + .runnables + .iter() + .filter(|r| r.session_id.is_some()) + .count(); + let background = state + .runnables + .iter() + .filter(|r| r.session_id.is_none()) + .count(); + (foreground, background) + } + + fn step(&self) -> bool { + self.step_filtered(false) + } + + fn step_filtered(&self, background_only: bool) -> bool { + let (elapsed_count, runnables_before) = { + let mut state = self.state.lock(); + let end_ix = state + .timers + .partition_point(|timer| timer.expiration <= self.clock.now()); + let elapsed: Vec<_> = state.timers.drain(..end_ix).collect(); + let count = elapsed.len(); + let runnables = state.runnables.len(); + drop(state); + // Dropping elapsed timers here wakes the waiting futures + drop(elapsed); + (count, runnables) + }; + + if elapsed_count > 0 { + let runnables_after = self.state.lock().runnables.len(); + if std::env::var("DEBUG_SCHEDULER").is_ok() { + eprintln!( + "[scheduler] Expired {} timers at {:?}, runnables: {} -> {}", + elapsed_count, + self.clock.now(), + runnables_before, + runnables_after + ); + } + return true; + } + + let runnable = { + let state = &mut *self.state.lock(); + + // Find candidate tasks: + // - For foreground tasks (with session_id), only the first task from each session + // is a candidate (to preserve intra-session ordering) + // - For background tasks (no session_id), all are candidates + // - Tasks from blocked sessions are excluded + // - If background_only is true, skip foreground tasks entirely + let mut seen_sessions = HashSet::new(); + let candidate_indices: Vec = state + .runnables + .iter() + .enumerate() + .filter(|(_, runnable)| { + if let Some(session_id) = runnable.session_id { + // Skip foreground tasks if background_only mode + if background_only { + return false; + } + // Exclude tasks from blocked sessions + if state.blocked_sessions.contains(&session_id) { + return false; + } + // Only include first task from each session (insert returns true if new) + seen_sessions.insert(session_id) + } else { + // Background tasks are always candidates + true + } + }) + .map(|(ix, _)| ix) + .collect(); + + if candidate_indices.is_empty() { + None + } else if state.randomize_order { + // Use priority-weighted random selection + let weights: Vec = candidate_indices + .iter() + .map(|&ix| state.runnables[ix].priority.weight()) + .collect(); + let total_weight: u32 = weights.iter().sum(); + + if total_weight == 0 { + // Fallback to uniform random if all weights are zero + let choice = self.rng.lock().random_range(0..candidate_indices.len()); + state.runnables.remove(candidate_indices[choice]) + } else { + let mut target = self.rng.lock().random_range(0..total_weight); + let mut selected_idx = 0; + for (i, &weight) in weights.iter().enumerate() { + if target < weight { + selected_idx = i; + break; + } + target -= weight; + } + state.runnables.remove(candidate_indices[selected_idx]) + } + } else { + // Non-randomized: just take the first candidate task + state.runnables.remove(candidate_indices[0]) + } + }; + + if let Some(runnable) = runnable { + let is_foreground = runnable.session_id.is_some(); + let was_main_thread = self.state.lock().is_main_thread; + self.state.lock().is_main_thread = is_foreground; + runnable.run(); + self.state.lock().is_main_thread = was_main_thread; + return true; + } + + false + } + + /// Drops all runnable tasks from the scheduler. + /// + /// This is used by the leak detector to ensure that all tasks have been dropped as tasks may keep entities alive otherwise. + /// Why do we even have tasks left when tests finish you may ask. The reason for that is simple, the scheduler itself is the executor and it retains the scheduled runnables. + /// A lot of tasks, including every foreground task contain an executor handle that keeps the test scheduler alive, causing a reference cycle, thus the need for this function right now. + pub fn drain_tasks(&self) { + // dropping runnables may reschedule tasks + // due to drop impls with executors in them + // so drop until we reach a fixpoint + loop { + let mut state = self.state.lock(); + if state.runnables.is_empty() && state.timers.is_empty() { + break; + } + let runnables = std::mem::take(&mut state.runnables); + let timers = std::mem::take(&mut state.timers); + drop(state); + drop(timers); + drop(runnables); + } + } + + pub 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 debug = std::env::var("DEBUG_SCHEDULER").is_ok(); + let start = self.clock.now(); + let next_now = start + duration; + if debug { + let timer_count = self.state.lock().timers.len(); + eprintln!( + "[scheduler] advance_clock({:?}) from {:?}, {} pending timers", + duration, start, timer_count + ); + } + loop { + self.run(); + if let Some(timer) = self.state.lock().timers.first() + && timer.expiration <= next_now + { + let advance_to = timer.expiration; + if debug { + eprintln!( + "[scheduler] Advancing clock {:?} -> {:?} for timer", + self.clock.now(), + advance_to + ); + } + self.clock.advance(advance_to - self.clock.now()); + } else { + break; + } + } + self.clock.advance(next_now - self.clock.now()); + if debug { + eprintln!( + "[scheduler] advance_clock done, now at {:?}", + self.clock.now() + ); + } + } + + fn park(&self, deadline: Option) -> bool { + if self.state.lock().allow_parking { + let start = Instant::now(); + // Enforce a hard timeout to prevent tests from hanging indefinitely + let hard_deadline = start + Duration::from_secs(15); + + // Use the earlier of the provided deadline or the hard timeout deadline + let effective_deadline = deadline + .map(|d| d.min(hard_deadline)) + .unwrap_or(hard_deadline); + + // Park in small intervals to allow checking both deadlines + const PARK_INTERVAL: Duration = Duration::from_millis(100); + loop { + let now = Instant::now(); + if now >= effective_deadline { + // Check if we hit the hard timeout + if now >= hard_deadline { + panic!( + "Test timed out after 15 seconds while parking. \ + This may indicate a deadlock or missing waker.", + ); + } + // Hit the provided deadline + return false; + } + + let remaining = effective_deadline.saturating_duration_since(now); + let park_duration = remaining.min(PARK_INTERVAL); + let before_park = Instant::now(); + thread::park_timeout(park_duration); + let elapsed = before_park.elapsed(); + + // Advance the test clock by the real elapsed time while parking + self.clock.advance(elapsed); + + // Check if any timers have expired after advancing the clock. + // If so, return so the caller can process them. + if self + .state + .lock() + .timers + .first() + .map_or(false, |t| t.expiration <= self.clock.now()) + { + return true; + } + + // Check if we were woken up by a different thread. + // We use a flag because timing-based detection is unreliable: + // OS scheduling delays can cause elapsed >= park_duration even when + // we were woken early by unpark(). + if std::mem::take(&mut self.state.lock().unparked) { + return 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" + ); + } + } +} + +fn assert_correct_thread(expected: &Thread, state: &Arc>) { + let current_thread = thread::current(); + let mut state = state.lock(); + if state.parking_allowed_once { + return; + } + if current_thread.id() == expected.id() { + return; + } + + let message = format!( + "Detected activity on thread {:?} {:?}, but test scheduler is running on {:?} {:?}. Your test is not deterministic.", + current_thread.name(), + current_thread.id(), + expected.name(), + expected.id(), + ); + let backtrace = Backtrace::new(); + if state.finished { + panic!("{}", message); + } else { + state.non_determinism_error = Some((message, backtrace)) + } +} + +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: Pin<&mut dyn Future>, + timeout: Option, + ) -> bool { + 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); + + let mut completed = false; + for _ in 0..max_ticks { + match future.as_mut().poll(&mut cx) { + Poll::Ready(()) => { + completed = true; + break; + } + Poll::Pending => {} + } + + 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 { + let parking_allowed = self.state.lock().allow_parking; + // In deterministic mode (parking forbidden), instantly jump to the next timer. + // In non-deterministic mode (parking allowed), let real time pass instead. + let advanced_to_timer = !parking_allowed && self.advance_clock_to_next_timer(); + if !advanced_to_timer && !self.park(deadline) { + break; + } + } + } + + if session_id.is_some() { + self.state.lock().blocked_sessions.pop(); + } + + completed + } + + fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable) { + assert_correct_thread(&self.thread, &self.state); + 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), + priority: Priority::default(), + runnable, + }, + ); + state.unparked = true; + drop(state); + self.thread.unpark(); + } + + fn schedule_background_with_priority( + &self, + runnable: Runnable, + priority: Priority, + ) { + assert_correct_thread(&self.thread, &self.state); + 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, + priority, + runnable, + }, + ); + state.unparked = true; + drop(state); + self.thread.unpark(); + } + + fn spawn_realtime(&self, f: Box) { + std::thread::spawn(move || { + f(); + }); + } + + #[track_caller] + 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) -> Option<&TestScheduler> { + Some(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: 1..=1000, + } + } +} + +struct ScheduledRunnable { + session_id: Option, + priority: Priority, + 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, + is_main_thread: bool, + non_determinism_error: Option<(String, Backtrace)>, + parking_allowed_once: bool, + finished: bool, + unparked: bool, +} + +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) { + assert_correct_thread(&self.thread, &self.state); + + 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) { + assert_correct_thread(&self.thread, &self.state); + + let mut state = self.state.lock(); + if let Some(id) = self.id { + state.pending_traces.remove(&id); + } + state.unparked = true; + drop(state); + 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); + +/// A wrapper around `Arc>` that provides convenient methods +/// for random number generation without requiring explicit locking. +#[derive(Clone)] +pub struct SharedRng(Arc>); + +impl SharedRng { + /// Lock the inner RNG for direct access. Use this when you need multiple + /// random operations without re-locking between each one. + pub fn lock(&self) -> MutexGuard<'_, StdRng> { + self.0.lock() + } + + /// Generate a random value in the given range. + pub fn random_range(&self, range: R) -> T + where + T: SampleUniform, + R: SampleRange, + { + self.0.lock().random_range(range) + } + + /// Generate a random boolean with the given probability of being true. + pub fn random_bool(&self, p: f64) -> bool { + self.0.lock().random_bool(p) + } + + /// Generate a random value of the given type. + pub fn random(&self) -> T + where + StandardUniform: Distribution, + { + self.0.lock().random() + } + + /// Generate a random ratio - true with probability `numerator/denominator`. + pub fn random_ratio(&self, numerator: u32, denominator: u32) -> bool { + self.0.lock().random_ratio(numerator, denominator) + } +} + +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/src/scheduler/tests.rs b/src/scheduler/tests.rs new file mode 100644 index 0000000000..03fe8075f9 --- /dev/null +++ b/src/scheduler/tests.rs @@ -0,0 +1,670 @@ +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, Waker}, +}; + +#[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 = "Parking forbidden. Pending traces:")] +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_many_with_arbitrary_seed() { + for seed in [0u64, 1, 5, 42] { + let mut seeds_seen = Vec::new(); + let iterations = 3usize; + + for current_seed in seed..seed + iterations as u64 { + let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed( + current_seed, + ))); + let captured_seed = current_seed; + scheduler + .foreground() + .block_on(async { seeds_seen.push(captured_seed) }); + scheduler.run(); + } + + assert_eq!( + seeds_seen, + (seed..seed + iterations as u64).collect::>(), + "Expected {iterations} iterations starting at seed {seed}" + ); + } +} + +#[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.ok(), Some(42)); + }); + + // Test case: future times out + TestScheduler::once(async |scheduler| { + // Make timeout behavior deterministic by forcing the timeout tick budget to be exactly 0. + // This prevents `block_with_timeout` from making progress via extra scheduler stepping and + // accidentally completing work that we expect to time out. + scheduler.set_timeout_ticks(0..=0); + + let foreground = scheduler.foreground(); + let future = future::pending::<()>(); + let output = foreground.block_with_timeout(Duration::from_millis(50), future); + assert!(output.is_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| { + // Keep the existing probabilistic behavior here (do not force 0 ticks), since this subtest + // is explicitly checking that some seeds/timeouts can complete while others can time out. + 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)] + ); + + // Regression test: + // A timed-out future must not be cancelled. The returned future should still be + // pollable to completion later. We also want to ensure time only advances when we + // explicitly advance it (not by yielding). + TestScheduler::once(async |scheduler| { + // Force immediate timeout: the timeout tick budget is 0 so we will not step or + // advance timers inside `block_with_timeout`. + scheduler.set_timeout_ticks(0..=0); + + let background = scheduler.background(); + + // This task should only complete once time is explicitly advanced. + let task = background.spawn({ + let scheduler = scheduler.clone(); + async move { + scheduler.timer(Duration::from_millis(100)).await; + 123 + } + }); + + // This should time out before we advance time enough for the timer to fire. + let timed_out = scheduler + .foreground() + .block_with_timeout(Duration::from_millis(50), task); + assert!( + timed_out.is_err(), + "expected timeout before advancing the clock enough for the timer" + ); + + // Now explicitly advance time and ensure the returned future can complete. + let mut task = timed_out.err().unwrap(); + scheduler.advance_clock(Duration::from_millis(100)); + scheduler.run(); + + let output = scheduler.foreground().block_on(&mut task); + assert_eq!(output, 123); + }); +} + +// 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 + } + } +} + +#[test] +fn test_nondeterministic_wake_detection() { + let config = TestSchedulerConfig { + allow_parking: false, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); + + // A future that captures its waker and sends it to an external thread + struct SendWakerToThread { + waker_tx: Option>, + } + + impl Future for SendWakerToThread { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if let Some(tx) = self.waker_tx.take() { + tx.send(cx.waker().clone()).ok(); + } + Poll::Ready(()) + } + } + + let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); + + // Get a waker by running a future that sends it + scheduler.foreground().block_on(SendWakerToThread { + waker_tx: Some(waker_tx), + }); + + // Spawn a real OS thread that will call wake() on the waker + let handle = std::thread::spawn(move || { + if let Ok(waker) = waker_rx.recv() { + // This should trigger the non-determinism detection + waker.wake(); + } + }); + + // Wait for the spawned thread to complete + handle.join().ok(); + + // The non-determinism error should be detected when end_test is called + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + scheduler.end_test(); + })); + assert!(result.is_err(), "Expected end_test to panic"); + let panic_payload = result.unwrap_err(); + let panic_message = panic_payload + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| panic_payload.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + panic_message.contains("Your test is not deterministic"), + "Expected panic message to contain non-determinism error, got: {}", + panic_message + ); +} + +#[test] +fn test_nondeterministic_wake_allowed_with_parking() { + let config = TestSchedulerConfig { + allow_parking: true, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); + + // A future that captures its waker and sends it to an external thread + struct WakeFromExternalThread { + waker_sent: bool, + waker_tx: Option>, + } + + impl Future for WakeFromExternalThread { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if !self.waker_sent { + self.waker_sent = true; + if let Some(tx) = self.waker_tx.take() { + tx.send(cx.waker().clone()).ok(); + } + Poll::Pending + } else { + Poll::Ready(()) + } + } + } + + let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); + + // Spawn a real OS thread that will call wake() on the waker + std::thread::spawn(move || { + if let Ok(waker) = waker_rx.recv() { + // With allow_parking, this should NOT panic + waker.wake(); + } + }); + + // This should complete without panicking + scheduler.foreground().block_on(WakeFromExternalThread { + waker_sent: false, + waker_tx: Some(waker_tx), + }); +} + +#[test] +fn test_nondeterministic_waker_drop_detection() { + let config = TestSchedulerConfig { + allow_parking: false, + ..Default::default() + }; + let scheduler = Arc::new(TestScheduler::new(config)); + + // A future that captures its waker and sends it to an external thread + struct SendWakerToThread { + waker_tx: Option>, + } + + impl Future for SendWakerToThread { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if let Some(tx) = self.waker_tx.take() { + tx.send(cx.waker().clone()).ok(); + } + Poll::Ready(()) + } + } + + let (waker_tx, waker_rx) = std::sync::mpsc::channel::(); + + // Get a waker by running a future that sends it + scheduler.foreground().block_on(SendWakerToThread { + waker_tx: Some(waker_tx), + }); + + // Spawn a real OS thread that will drop the waker without calling wake + let handle = std::thread::spawn(move || { + if let Ok(waker) = waker_rx.recv() { + // This should trigger the non-determinism detection on drop + drop(waker); + } + }); + + // Wait for the spawned thread to complete + handle.join().ok(); + + // The non-determinism error should be detected when end_test is called + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + scheduler.end_test(); + })); + assert!(result.is_err(), "Expected end_test to panic"); + let panic_payload = result.unwrap_err(); + let panic_message = panic_payload + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| panic_payload.downcast_ref::<&str>().copied()) + .unwrap_or(""); + assert!( + panic_message.contains("Your test is not deterministic"), + "Expected panic message to contain non-determinism error, got: {}", + panic_message + ); +} + +#[test] +fn test_background_priority_scheduling() { + use parking_lot::Mutex; + + // Run many iterations to get statistical significance + let mut high_before_low_count = 0; + let iterations = 100; + + for seed in 0..iterations { + let config = TestSchedulerConfig::with_seed(seed); + let scheduler = Arc::new(TestScheduler::new(config)); + let background = scheduler.background(); + + let execution_order = Arc::new(Mutex::new(Vec::new())); + + // Spawn low priority tasks first + for i in 0..3 { + let order = execution_order.clone(); + background + .spawn_with_priority(Priority::Low, async move { + order.lock().push(format!("low-{}", i)); + }) + .detach(); + } + + // Spawn high priority tasks second + for i in 0..3 { + let order = execution_order.clone(); + background + .spawn_with_priority(Priority::High, async move { + order.lock().push(format!("high-{}", i)); + }) + .detach(); + } + + scheduler.run(); + + // Count how many high priority tasks ran in the first half + let order = execution_order.lock(); + let high_in_first_half = order + .iter() + .take(3) + .filter(|s| s.starts_with("high")) + .count(); + + if high_in_first_half >= 2 { + high_before_low_count += 1; + } + } + + // High priority tasks should tend to run before low priority tasks + // With weights of 60 vs 10, high priority should dominate early execution + assert!( + high_before_low_count > iterations / 2, + "Expected high priority tasks to run before low priority tasks more often. \ + Got {} out of {} iterations", + high_before_low_count, + iterations + ); +} diff --git a/src/styled.rs b/src/styled.rs index 752038c1ed..f1d2e98942 100644 --- a/src/styled.rs +++ b/src/styled.rs @@ -5,7 +5,7 @@ use crate::{ StyleRefinement, TextAlign, TextOverflow, TextStyleRefinement, UnderlineStyle, WhiteSpace, px, relative, rems, }; -pub use gpui_macros::{ +pub use gpui_ce_macros::{ border_style_methods, box_shadow_style_methods, cursor_style_methods, margin_style_methods, overflow_style_methods, padding_style_methods, position_style_methods, visibility_style_methods, @@ -17,21 +17,21 @@ const ELLIPSIS: SharedString = SharedString::new_static("…"); // 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( all(any(feature = "inspector", debug_assertions), not(rust_analyzer)), - gpui_macros::derive_inspector_reflection + gpui_ce_macros::derive_inspector_reflection )] pub trait Styled: Sized { /// Returns a reference to the style memory of this element. fn style(&mut self) -> &mut StyleRefinement; - gpui_macros::style_helpers!(); - gpui_macros::visibility_style_methods!(); - gpui_macros::margin_style_methods!(); - gpui_macros::padding_style_methods!(); - gpui_macros::position_style_methods!(); - gpui_macros::overflow_style_methods!(); - gpui_macros::cursor_style_methods!(); - gpui_macros::border_style_methods!(); - gpui_macros::box_shadow_style_methods!(); + gpui_ce_macros::style_helpers!(); + gpui_ce_macros::visibility_style_methods!(); + gpui_ce_macros::margin_style_methods!(); + gpui_ce_macros::padding_style_methods!(); + gpui_ce_macros::position_style_methods!(); + gpui_ce_macros::overflow_style_methods!(); + gpui_ce_macros::cursor_style_methods!(); + gpui_ce_macros::border_style_methods!(); + gpui_ce_macros::box_shadow_style_methods!(); /// Sets the display type of the element to `block`. /// [Docs](https://tailwindcss.com/docs/display) diff --git a/src/test.rs b/src/test.rs index 5ae72d2be1..3be0a59678 100644 --- a/src/test.rs +++ b/src/test.rs @@ -27,7 +27,6 @@ //! ``` use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; use futures::StreamExt as _; -use rand::prelude::*; use smol::channel; use std::{ env, @@ -54,7 +53,7 @@ pub fn run_test( eprintln!("seed = {seed}"); } let result = panic::catch_unwind(|| { - let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(seed)); + let dispatcher = TestDispatcher::new(seed); test_fn(dispatcher, seed); }); diff --git a/src/text_system/line_wrapper.rs b/src/text_system/line_wrapper.rs index e4694f2005..9ac3e680e0 100644 --- a/src/text_system/line_wrapper.rs +++ b/src/text_system/line_wrapper.rs @@ -316,10 +316,9 @@ impl Boundary { mod tests { use super::*; use crate::{Font, FontFeatures, FontStyle, FontWeight, TestAppContext, TestDispatcher, font}; - use rand::prelude::*; fn build_wrapper() -> LineWrapper { - let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); + let dispatcher = TestDispatcher::new(0); let cx = TestAppContext::build(dispatcher, None); let id = cx.text_system().resolve_font(&font(".ZedMono")); LineWrapper::new(id, px(16.), cx.text_system().platform_text_system.clone()) diff --git a/tooling/macros/Cargo.lock b/tooling/macros/Cargo.lock new file mode 100644 index 0000000000..7b6fdb598b --- /dev/null +++ b/tooling/macros/Cargo.lock @@ -0,0 +1,54 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "gpui-ce-macros" +version = "0.1.0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/tooling/macros/Cargo.toml b/tooling/macros/Cargo.toml new file mode 100644 index 0000000000..d549a73560 --- /dev/null +++ b/tooling/macros/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "gpui-ce-macros" +version = "0.1.0" +edition = "2024" +publish = false +license = "Apache-2.0" +description = "Macros used by gpui" + +[features] +inspector = [] + +[lib] +path = "src/gpui_macros.rs" +proc-macro = true +doctest = true + +[dependencies] +heck = "0.5.0" +proc-macro2 = "1.0.106" +quote = "1.0.45" +syn = { version = "2.0.117", features = ["full", "visit-mut", "extra-traits"] } + +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(rust_analyzer)'] } + +[dev-dependencies] +gpui = { path = "../..", package = "gpui-ce" } diff --git a/tooling/macros/src/derive_action.rs b/tooling/macros/src/derive_action.rs new file mode 100644 index 0000000000..4e6c6277e4 --- /dev/null +++ b/tooling/macros/src/derive_action.rs @@ -0,0 +1,211 @@ +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/tooling/macros/src/derive_app_context.rs b/tooling/macros/src/derive_app_context.rs new file mode 100644 index 0000000000..caa4162dc2 --- /dev/null +++ b/tooling/macros/src/derive_app_context.rs @@ -0,0 +1,112 @@ +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, + ) -> gpui::Entity { + self.#app_variable.new(build_entity) + } + + fn reserve_entity(&mut self) -> gpui::Reservation { + self.#app_variable.reserve_entity() + } + + fn insert_entity( + &mut self, + reservation: gpui::Reservation, + build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T, + ) -> gpui::Entity { + 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, + ) -> R + where + T: 'static, + { + self.#app_variable.update_entity(handle, update) + } + + fn as_mut<'y, 'z, T>( + &'y mut self, + handle: &'z gpui::Entity, + ) -> gpui::GpuiBorrow<'y, T> + where + T: 'static, + { + self.#app_variable.as_mut(handle) + } + + fn read_entity( + &self, + handle: &gpui::Entity, + read: impl FnOnce(&T, &gpui::App) -> R, + ) -> R + 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) -> R + where + G: gpui::Global, + { + self.#app_variable.read_global(callback) + } + } + }; + + r#gen.into() +} diff --git a/tooling/macros/src/derive_inspector_reflection.rs b/tooling/macros/src/derive_inspector_reflection.rs new file mode 100644 index 0000000000..9c1cb503a8 --- /dev/null +++ b/tooling/macros/src/derive_inspector_reflection.rs @@ -0,0 +1,305 @@ +//! 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/tooling/macros/src/derive_into_element.rs b/tooling/macros/src/derive_into_element.rs new file mode 100644 index 0000000000..89d609ae65 --- /dev/null +++ b/tooling/macros/src/derive_into_element.rs @@ -0,0 +1,24 @@ +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/tooling/macros/src/derive_render.rs b/tooling/macros/src/derive_render.rs new file mode 100644 index 0000000000..3e0dcbc993 --- /dev/null +++ b/tooling/macros/src/derive_render.rs @@ -0,0 +1,21 @@ +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/tooling/macros/src/derive_visual_context.rs b/tooling/macros/src/derive_visual_context.rs new file mode 100644 index 0000000000..79f8d599d8 --- /dev/null +++ b/tooling/macros/src/derive_visual_context.rs @@ -0,0 +1,71 @@ +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, + ) -> R { + 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, + ) -> gpui::Entity { + 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, + ) -> gpui::Entity + where + V: 'static + gpui::Render, + { + self.#window_variable.replace_root(self.#app_variable, build_view) + } + + fn focus(&mut self, entity: &gpui::Entity) + 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/tooling/macros/src/gpui_macros.rs b/tooling/macros/src/gpui_macros.rs new file mode 100644 index 0000000000..e30c85e6ed --- /dev/null +++ b/tooling/macros/src/gpui_macros.rs @@ -0,0 +1,297 @@ +mod derive_action; +mod derive_app_context; +mod derive_into_element; +mod derive_render; +mod derive_visual_context; +mod property_test; +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) +} + +/// A variant of `#[gpui::test]` that supports property-based testing. +/// +/// A property test, much like a standard GPUI randomized test, allows testing +/// claims of the form "for any possible X, Y should hold". For example: +/// ``` +/// #[gpui::property_test] +/// fn test_arithmetic(x: i32, y: i32) { +/// assert!(x == y || x < y || x > y); +/// } +/// ``` +/// Standard GPUI randomized tests provide you with an instance of `StdRng` to +/// generate random data in a controlled manner. Property-based tests have some +/// advantages, however: +/// - Shrinking - the harness also understands a notion of the "complexity" of a +/// particular value. This allows it to find the "simplest possible value that +/// causes the test to fail". +/// - Ergonomics/clarity - the property-testing harness will automatically +/// generate values, removing the need to fill the test body with generation +/// logic. +/// - Failure persistence - if a failing seed is identified, it is stored in a +/// file, which can be checked in, and future runs will check these cases before +/// future cases. +/// +/// Property tests work best when all inputs can be generated up-front and kept +/// in a simple data structure. Sometimes, this isn't possible - for example, if +/// a test needs to make a random decision based on the current state of some +/// structure. In this case, a standard GPUI randomized test may be more +/// suitable. +/// +/// ## Customizing random values +/// +/// This macro is based on the [`#[proptest::property_test]`] macro, but handles +/// some of the same GPUI-specific arguments as `#[gpui::test]`. Specifically, +/// `&{mut,} TestAppContext` and `BackgroundExecutor` work as normal. `StdRng` +/// arguments are **explicitly forbidden**, since they break shrinking, and are +/// a common footgun. +/// +/// All other arguments are forwarded to the underlying proptest macro. +/// +/// Note: much of the following is copied from the proptest docs, specifically the +/// [`#[proptest::property_test]`] macro docs. +/// +/// Random values of type `T` are generated by a `Strategy` object. +/// Some types have a canonical `Strategy` - these types also implement +/// `Arbitrary`. Parameters to a `#[gpui::property_test]`, by default, use a +/// type's `Arbitrary` implementation. If you'd like to provide a custom +/// strategy, you can use `#[strategy = ...]` on the argument: +/// ``` +/// #[gpui::property_test] +/// fn int_test(#[strategy = 1..10] x: i32, #[strategy = "[a-zA-Z0-9]{20}"] s: String) { +/// assert!(s.len() > (x as usize)); +/// } +/// ``` +/// +/// For more information on writing custom `Strategy` and `Arbitrary` +/// implementations, see [the proptest book][book], and the [`Strategy`] trait. +/// +/// ## Scheduler +/// +/// Similar to `#[gpui::test]`, this macro will choose random seeds for the test +/// scheduler. It uses `.no_shrink()` to tell proptest that all seeds are +/// roughly equivalent in terms of "complexity". If `$SEED` is set, it will +/// affect **ONLY** the seed passed to the scheduler. To control other values, +/// use custom `Strategy`s. +/// +/// [`#[proptest::property_test]`]: https://docs.rs/proptest/latest/proptest/attr.property_test.html +/// [book]: https://proptest-rs.github.io/proptest/intro.html +/// [`Strategy`]: https://docs.rs/proptest/latest/proptest/strategy/trait.Strategy.html +#[proc_macro_attribute] +pub fn property_test(args: TokenStream, function: TokenStream) -> TokenStream { + property_test::test(args.into(), function.into()).into() +} + +/// 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/tooling/macros/src/property_test.rs b/tooling/macros/src/property_test.rs new file mode 100644 index 0000000000..6bf60eca1b --- /dev/null +++ b/tooling/macros/src/property_test.rs @@ -0,0 +1,199 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote, quote_spanned}; +use syn::{ + FnArg, Ident, ItemFn, Type, parse2, punctuated::Punctuated, spanned::Spanned, token::Comma, +}; + +pub fn test(args: TokenStream, item: TokenStream) -> TokenStream { + let item_span = item.span(); + let Ok(func) = parse2::(item) else { + return quote_spanned! { item_span => + compile_error!("#[gpui::property_test] must be placed on a function"); + }; + }; + + let test_name = func.sig.ident.clone(); + let inner_fn_name = format_ident!("__{test_name}"); + + let parsed_args = parse_args(func.sig.inputs, &test_name); + + let inner_body = func.block; + let inner_arg_decls = parsed_args.inner_fn_decl_args; + let asyncness = func.sig.asyncness; + + let inner_fn = quote! { + let #inner_fn_name = #asyncness move |#inner_arg_decls| #inner_body; + }; + + let arg_errors = parsed_args.errors; + let proptest_args = parsed_args.proptest_args; + let inner_args = parsed_args.inner_fn_args; + let cx_vars = parsed_args.cx_vars; + let cx_teardowns = parsed_args.cx_teardowns; + + let proptest_args = quote! { + #[strategy = ::gpui::seed_strategy()] __seed: u64, + #proptest_args + }; + + let run_test_body = match &asyncness { + None => quote! { + #cx_vars + #inner_fn_name(#inner_args); + #cx_teardowns + }, + Some(_) => quote! { + let foreground_executor = gpui::ForegroundExecutor::new(std::sync::Arc::new(dispatcher.clone())); + #cx_vars + foreground_executor.block_test(#inner_fn_name(#inner_args)); + #cx_teardowns + }, + }; + + quote! { + #arg_errors + + #[::gpui::proptest::property_test(proptest_path = "::gpui::proptest", #args)] + fn #test_name(#proptest_args) { + #inner_fn + + ::gpui::run_test_once( + __seed, + Box::new(move |dispatcher| { + #run_test_body + }), + ) + } + } +} + +#[derive(Default)] +struct ParsedArgs { + cx_vars: TokenStream, + cx_teardowns: TokenStream, + proptest_args: TokenStream, + errors: TokenStream, + + // exprs passed at the call-site + inner_fn_args: TokenStream, + // args in the declaration + inner_fn_decl_args: TokenStream, +} + +fn parse_args(args: Punctuated, test_name: &Ident) -> ParsedArgs { + let mut parsed = ParsedArgs::default(); + let mut args = args.into_iter().collect(); + + remove_cxs(&mut parsed, &mut args, test_name); + remove_std_rng(&mut parsed, &mut args); + remove_background_executor(&mut parsed, &mut args); + + // all remaining args forwarded to proptest's macro + parsed.proptest_args = quote!( #(#args),* ); + + parsed +} + +fn remove_cxs(parsed: &mut ParsedArgs, args: &mut Vec, test_name: &Ident) { + let mut ix = 0; + args.retain_mut(|arg| { + if !is_test_cx(arg) { + return true; + } + + let cx_varname = format_ident!("cx_{ix}"); + ix += 1; + + parsed.cx_vars.extend(quote!( + let mut #cx_varname = gpui::TestAppContext::build( + dispatcher.clone(), + Some(stringify!(#test_name)), + ); + )); + parsed.cx_teardowns.extend(quote!( + dispatcher.run_until_parked(); + #cx_varname.executor().forbid_parking(); + #cx_varname.quit(); + dispatcher.run_until_parked(); + )); + + parsed.inner_fn_decl_args.extend(quote!(#arg,)); + parsed.inner_fn_args.extend(quote!(&mut #cx_varname,)); + + false + }); +} + +fn remove_std_rng(parsed: &mut ParsedArgs, args: &mut Vec) { + args.retain_mut(|arg| { + if !is_std_rng(arg) { + return true; + } + + parsed.errors.extend(quote_spanned! { arg.span() => + compile_error!("`StdRng` is not allowed in a property test. Consider implementing `Arbitrary`, or implementing a custom `Strategy`. https://altsysrq.github.io/proptest-book/proptest/tutorial/strategy-basics.html"); + }); + + false + }); +} + +fn remove_background_executor(parsed: &mut ParsedArgs, args: &mut Vec) { + args.retain_mut(|arg| { + if !is_background_executor(arg) { + return true; + } + + parsed.inner_fn_decl_args.extend(quote!(#arg,)); + parsed + .inner_fn_args + .extend(quote!(gpui::BackgroundExecutor::new(std::sync::Arc::new( + dispatcher.clone() + )),)); + + false + }); +} + +// Matches `&TestAppContext` or `&foo::bar::baz::TestAppContext` +fn is_test_cx(arg: &FnArg) -> bool { + let FnArg::Typed(arg) = arg else { + return false; + }; + + let Type::Reference(ty) = &*arg.ty else { + return false; + }; + + let Type::Path(ty) = &*ty.elem else { + return false; + }; + + ty.path + .segments + .last() + .is_some_and(|seg| seg.ident == "TestAppContext") +} + +fn is_std_rng(arg: &FnArg) -> bool { + is_path_with_last_segment(arg, "StdRng") +} + +fn is_background_executor(arg: &FnArg) -> bool { + is_path_with_last_segment(arg, "BackgroundExecutor") +} + +fn is_path_with_last_segment(arg: &FnArg, last_segment: &str) -> bool { + let FnArg::Typed(arg) = arg else { + return false; + }; + + let Type::Path(ty) = &*arg.ty else { + return false; + }; + + ty.path + .segments + .last() + .is_some_and(|seg| seg.ident == last_segment) +} diff --git a/tooling/macros/src/register_action.rs b/tooling/macros/src/register_action.rs new file mode 100644 index 0000000000..ca36ce3186 --- /dev/null +++ b/tooling/macros/src/register_action.rs @@ -0,0 +1,47 @@ +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/tooling/macros/src/styles.rs b/tooling/macros/src/styles.rs new file mode 100644 index 0000000000..133c9fdebe --- /dev/null +++ b/tooling/macros/src/styles.rs @@ -0,0 +1,1472 @@ +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.", + }, + BoxStylePrefix { + prefix: "min_size", + auto_allowed: true, + fields: vec![quote! {min_size.width}, quote! {min_size.height}], + doc_string_prefix: "Sets the minimum width and height of the element.", + }, + 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)", + }, + BoxStylePrefix { + prefix: "max_size", + auto_allowed: true, + fields: vec![quote! {max_size.width}, quote! {max_size.height}], + doc_string_prefix: "Sets the maximum 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: "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/tooling/macros/src/test.rs b/tooling/macros/src/test.rs new file mode 100644 index 0000000000..087e01740d --- /dev/null +++ b/tooling/macros/src/test.rs @@ -0,0 +1,347 @@ +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)), + ); + let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle(); + )); + cx_teardowns.extend(quote!( + #cx_varname.run_until_parked(); + #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); + #cx_varname.run_until_parked(); + drop(#cx_varname); + )); + 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 exec = std::sync::Arc::new(dispatcher.clone()); + #cx_vars + gpui::ForegroundExecutor::new(exec.clone()).block_test(#inner_fn_name(#inner_fn_args)); + drop(exec); + #cx_teardowns + // Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity + // cycles as cancelled tasks will be dropped properly once the runnable gets run again + // + // async-task does not give us the power to do this just yet though + dispatcher.drain_tasks(); + drop(dispatcher); + }, + #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(); + let _entity_refcounts = #cx_varname_lock.ref_counts_drop_handle(); + )); + inner_fn_args.extend(quote!(&mut #cx_varname_lock,)); + cx_teardowns.extend(quote!( + drop(#cx_varname_lock); + #cx_varname.run_until_parked(); + #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); + #cx_varname.run_until_parked(); + drop(#cx_varname); + )); + 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)) + ); + let _entity_refcounts = #cx_varname.app.borrow().ref_counts_drop_handle(); + )); + cx_teardowns.extend(quote!( + #cx_varname.run_until_parked(); + #cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); }); + #cx_varname.run_until_parked(); + drop(#cx_varname); + )); + 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 + // Ideally we would only drop cancelled tasks, that way we could detect leaks due to task <-> entity + // cycles as cancelled tasks will be dropped properly once they runnable gets run again + // + // async-task does not give us the power to do this just yet though + dispatcher.drain_tasks(); + drop(dispatcher); + }, + #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/tooling/macros/tests/derive_context.rs b/tooling/macros/tests/derive_context.rs new file mode 100644 index 0000000000..8a1a6c226c --- /dev/null +++ b/tooling/macros/tests/derive_context.rs @@ -0,0 +1,14 @@ +#[test] +#[ignore = "VisualContext trait shape differs between wgpui and gpui-ce macro output"] +fn test_derive_context() { + use gpui::{App, Window}; + use gpui_ce_macros::{AppContext, VisualContext}; + + #[derive(AppContext, VisualContext)] + struct _MyCustomContext<'a, 'b> { + #[app] + app: &'a mut App, + #[window] + window: &'b mut Window, + } +} \ No newline at end of file diff --git a/tooling/macros/tests/derive_inspector_relfection.rs b/tooling/macros/tests/derive_inspector_relfection.rs new file mode 100644 index 0000000000..f6d75d7b1b --- /dev/null +++ b/tooling/macros/tests/derive_inspector_relfection.rs @@ -0,0 +1,133 @@ +//! 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_ce_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/tooling/macros/tests/render_test.rs b/tooling/macros/tests/render_test.rs new file mode 100644 index 0000000000..bf33d2dcbd --- /dev/null +++ b/tooling/macros/tests/render_test.rs @@ -0,0 +1,7 @@ +#[test] +fn test_derive_render() { + use gpui_ce_macros::Render; + + #[derive(Render)] + struct _Element; +} diff --git a/tooling/perf/Cargo.toml b/tooling/perf/Cargo.toml new file mode 100644 index 0000000000..c9018de010 --- /dev/null +++ b/tooling/perf/Cargo.toml @@ -0,0 +1,32 @@ +[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.md b/tooling/perf/LICENSE.md new file mode 100644 index 0000000000..f0608a63ae --- /dev/null +++ b/tooling/perf/LICENSE.md @@ -0,0 +1 @@ +../../LICENSE.md \ No newline at end of file diff --git a/tooling/perf/src/implementation.rs b/tooling/perf/src/implementation.rs new file mode 100644 index 0000000000..c151dda91f --- /dev/null +++ b/tooling/perf/src/implementation.rs @@ -0,0 +1,450 @@ +//! 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 new file mode 100644 index 0000000000..7933e66e79 --- /dev/null +++ b/tooling/perf/src/lib.rs @@ -0,0 +1,7 @@ +//! 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 new file mode 100644 index 0000000000..243658e508 --- /dev/null +++ b/tooling/perf/src/main.rs @@ -0,0 +1,582 @@ +//! 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); +} diff --git a/tests/action_macros.rs b/tooling/tests/action_macros.rs similarity index 100% rename from tests/action_macros.rs rename to tooling/tests/action_macros.rs