Start reorganizing examples (#10)

* Update crate name

* wip

* reduce to just the gpui crate

* Fix misisng example images

* Delete ownership_post.rs

* Update Cargo.toml

* Update README.md

* bump to 0.3.3

* organize

* organize

* Add more learn examples

* Delete _ownership_and_data_flow.rs

* wip learn examples

* Fix colors

* tidy

* Move _template up

* add animation example

* Update examples

* remove legacy examples from toml

* Delete examples.toml
This commit is contained in:
Nate Butler
2025-12-28 23:14:33 -05:00
committed by GitHub
parent 6a74dbe3f4
commit 18aeb44a54
295 changed files with 5174 additions and 23483 deletions
Generated
+689 -1088
View File
File diff suppressed because it is too large Load Diff
+358 -247
View File
@@ -1,227 +1,232 @@
[workspace]
resolver = "2"
members = [
"crates/collections",
"crates/gpui",
"crates/gpui_macros",
"crates/gpui_tokio",
"crates/html_to_markdown",
"crates/http_client",
"crates/http_client_tls",
"crates/media",
"crates/refineable",
"crates/refineable/derive_refineable",
"crates/reqwest_client",
"crates/scheduler",
"crates/sum_tree",
"crates/util",
"crates/util_macros",
"crates/watch",
"tooling/perf",
]
[workspace.package]
publish = false
[package]
name = "gpui-ce"
version = "0.3.3"
edition = "2024"
authors = ["Nathan Sobo <nathan@zed.dev>"]
description = "A community fork of Zed's GPU-accelerated UI framework"
repository = "https://github.com/gpui-ce/gpui-ce"
publish = true
license = "Apache-2.0"
homepage = "https://gpui-ce.github.io/"
readme = "README.md"
keywords = ["desktop", "gui", "immediate"]
categories = ["gui"]
autoexamples = false
[workspace.dependencies]
[features]
default = ["font-kit", "wayland", "x11", "windows-manifest"]
test-support = [
"leak-detection",
"collections/test-support",
"util/test-support",
"http_client/test-support",
"wayland",
"x11",
]
inspector = ["gpui_macros/inspector"]
leak-detection = ["backtrace"]
runtime_shaders = []
macos-blade = [
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"objc2",
"objc2-metal",
]
wayland = [
"bitflags",
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"ashpd/wayland",
"cosmic-text",
"font-kit",
"calloop-wayland-source",
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"filedescriptor",
"xkbcommon",
"open",
]
x11 = [
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"ashpd",
"cosmic-text",
"font-kit",
"as-raw-xcb-connection",
"x11rb",
"xkbcommon",
"xim",
"x11-clipboard",
"filedescriptor",
"open",
"scap?/x11",
]
screen-capture = [
"scap",
]
windows-manifest = []
#
# Workspace member crates
#
[lib]
name = "gpui"
path = "src/gpui.rs"
doctest = false
collections = { path = "crates/collections", version = "0.1.0" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
gpui = { path = "crates/gpui", default-features = false }
gpui_macros = { path = "crates/gpui_macros" }
gpui_tokio = { path = "crates/gpui_tokio" }
html_to_markdown = { path = "crates/html_to_markdown" }
http_client = { path = "crates/http_client" }
http_client_tls = { path = "crates/http_client_tls" }
media = { path = "crates/media" }
refineable = { path = "crates/refineable" }
reqwest_client = { path = "crates/reqwest_client" }
scheduler = { path = "crates/scheduler" }
sum_tree = { path = "crates/sum_tree" }
util = { path = "crates/util" }
util_macros = { path = "crates/util_macros" }
watch = { path = "crates/watch" }
perf = { path = "tooling/perf" }
#
# External crates
#
aho-corasick = "1.1"
circular-buffer = "1.0"
any_vec = "0.14"
[dependencies]
anyhow = "1.0.86"
arrayvec = { version = "0.7.4", features = ["serde"] }
ashpd = { version = "0.11", default-features = false, features = ["async-std"] }
async-compat = "0.2.1"
async-compression = { version = "0.4", features = ["gzip", "futures-io"] }
async-dispatcher = "0.1"
async-fs = "2.1"
async-lock = "2.1"
async-recursion = "1.0.0"
async-tar = "0.5.1"
async-task = "4.7"
async-trait = "0.1"
async-tungstenite = "0.31.0"
async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] }
backtrace = "0.3"
base64 = "0.22"
bincode = "1.2.1"
bitflags = "2.6.0"
blade-graphics = { version = "0.7.0" }
blade-macros = { version = "0.3.0" }
blade-util = { version = "0.3.0" }
brotli = "8.0.2"
bytes = "1.0"
cfg-if = "1.0.3"
chrono = { version = "0.4", features = ["serde"] }
cocoa = "=0.26.0"
cocoa-foundation = "=0.2.0"
convert_case = "0.8.0"
core-foundation = "=0.10.0"
core-foundation-sys = "0.8.6"
core-video = { version = "0.4.3", features = ["metal"] }
crossbeam = "0.8.4"
backtrace = { version = "0.3", optional = true }
bitflags = { version = "2.6.0", optional = true }
blade-graphics = { version = "0.7.0", optional = true }
blade-macros = { version = "0.3.0", optional = true }
blade-util = { version = "0.3.0", optional = true }
bytemuck = { version = "1", optional = true }
circular-buffer = "1.0"
collections = { package = "gpui_collections", version = "0.2.2" }
ctor = "0.4.0"
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
env_logger = "0.11"
etagere = "0.2"
futures = "0.3"
futures-lite = "1.13"
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
heck = "0.5"
hex = "0.4.3"
html5ever = "0.27.0"
indoc = "2"
inventory = "0.3.19"
http = "1.1"
http-body = "1.0"
hyper = "0.14"
gpui_macros = { package = "gpui-macros", version = "0.2.2" }
http_client = { package = "gpui_http_client", version = "0.2.2" }
image = "0.25.1"
indexmap = { version = "2.7.0", features = ["serde"] }
inventory = "0.3.19"
itertools = "0.14.0"
libc = "0.2"
linkify = "0.10.0"
log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] }
mach2 = "0.5"
markup5ever_rcdom = "0.3.0"
metal = "0.29"
naga = { version = "25.0", features = ["wgsl-in"] }
nix = "0.29"
objc = "0.2"
objc2-foundation = { version = "=0.3.1", default-features = false, features = [
"NSArray",
"NSAttributedString",
"NSBundle",
"NSCoder",
"NSData",
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSError",
"NSGeometry",
"NSNotification",
"NSNull",
"NSObjCRuntime",
"NSObject",
"NSProcessInfo",
"NSRange",
"NSRunLoop",
"NSString",
"NSURL",
"NSUndoManager",
"NSValue",
"objc2-core-foundation",
"std"
] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }
lyon = "1.0"
num_cpus = "1.13"
parking = "2.0.0"
parking_lot = "0.12.1"
pathdiff = "0.2"
pin-project = "1.1.10"
postage = { version = "0.5", features = ["futures-traits"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
proc-macro2 = "1.0.93"
profiling = "1"
pulldown-cmark = { version = "0.12.0", default-features = false }
quote = "1.0.9"
rand = "0.9"
rayon = "1.8"
regex = "1.5"
# WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [
"charset",
"http2",
"macos-system-configuration",
"multipart",
"rustls-tls-native-roots",
"socks",
"stream",
], package = "zed-reqwest", version = "0.12.15-zed" }
rust-embed = { version = "8.4", features = ["include-exclude"] }
rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
# WARNING: If you change this, you must also publish a new version of zed-scap to crates.io
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed" }
raw-window-handle = "0.6"
refineable = { package = "gpui_refineable", version = "0.2.2" }
resvg = { version = "0.45.0", default-features = false, features = [
"text",
"system-fonts",
"memmap-fonts",
] }
schemars = { version = "1.0", features = ["indexmap2"] }
seahash = "4.1"
semver = { version = "1.0", features = ["serde"] }
serde = { version = "1.0.221", features = ["derive", "rc"] }
serde_json = { version = "1.0.144", features = ["preserve_order", "raw_value"] }
serde_json_lenient = { version = "0.2", features = [
"preserve_order",
"raw_value",
] }
serde_urlencoded = "0.7"
sha2 = "0.10"
shlex = "1.3.0"
shellexpand = "2.1.0"
slotmap = "1.0.6"
smallvec = { version = "1.6", features = ["union", "const_new"] }
smallvec = { version = "1.6", features = ["union"] }
smol = "2.0"
spin = "0.10.0"
stacksafe = "0.1"
strsim = "0.11"
strum = { version = "0.27.2", features = ["derive"] }
subtle = "2.5.0"
syn = { version = "2.0.101", features = ["full", "extra-traits", "visit-mut"] }
sys-locale = "0.3.1"
sysinfo = "0.37.0"
take-until = "0.2.0"
tempfile = "3.20.0"
sum_tree = { package = "gpui_sum_tree", version = "0.2.2" }
taffy = "=0.9.0"
thiserror = "2.0.12"
time = { version = "0.3", features = [
"macros",
"parsing",
"serde",
"serde-well-known",
"formatting",
"local-offset",
] }
tokio = { version = "1" }
tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] }
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] }
tracing = "0.1.40"
unicase = "2.6"
unicode-segmentation = "1.10"
url = "2.2"
util = { package = "gpui_util", version = "0.2.2" }
util_macros = { package = "gpui_util_macros", version = "0.2.2" }
usvg = { version = "0.45.0", default-features = false }
uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] }
walkdir = "2.5"
which = "6.0.0"
windows-core = "0.61"
yawc = "0.2.5"
zeroize = "1.8"
ciborium = "0.2"
waker-fn = "1.2.0"
[workspace.dependencies.windows]
version = "0.61"
features = [
[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"
cocoa = "=0.26.0"
cocoa-foundation = "=0.2.0"
core-foundation = "=0.10.0"
core-foundation-sys = "0.8.6"
core-graphics = "0.24"
core-text = "21"
core-video = { version = "0.4.3", features = ["metal"] }
flume = "0.11"
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
foreign-types = "0.5"
mach2 = "0.5"
media = { package = "gpui_media", version = "0.2.2" }
metal = "0.29"
objc = "0.2"
objc2 = { version = "0.6", optional = true }
objc2-metal = { version = "0.3", optional = true }
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
pathfinder_geometry = "0.5"
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
scap = { git = "https://github.com/zed-industries/scap", rev = "4afea48c3b002197176fb19cd0f9b180dd36eaac", default-features = false, package = "zed-scap", version = "0.0.8-zed", optional = true }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
flume = "0.11"
oo7 = { version = "0.5.0", default-features = false, features = [
"async-std",
"native_crypto",
] }
ashpd = { version = "0.11", default-features = false, features = ["async-std"], optional = true }
blade-graphics = { version = "0.7.0", optional = true }
blade-macros = { version = "0.3.0", optional = true }
blade-util = { version = "0.3.0", optional = true }
bytemuck = { version = "1", optional = true }
cosmic-text = { version = "0.14.0", optional = true }
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [
"source-fontconfig-dlopen",
], optional = true }
calloop = "0.14.3"
filedescriptor = { version = "0.8.2", optional = true }
open = { version = "5.2.0", optional = true }
calloop-wayland-source = { version = "0.4.1", optional = true }
wayland-backend = { version = "0.3.3", features = [
"client_system",
"dlopen",
], optional = true }
wayland-client = { version = "0.31.2", optional = true }
wayland-cursor = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.31.2", features = [
"client",
"staging",
"unstable",
], optional = true }
wayland-protocols-plasma = { version = "0.2.0", features = [
"client",
], optional = true }
wayland-protocols-wlr = { version = "0.3.9", features = [
"client",
], optional = true }
as-raw-xcb-connection = { version = "1", optional = true }
x11rb = { version = "0.13.1", features = [
"allow-unsafe-code",
"xkb",
"randr",
"xinput",
"cursor",
"resource_manager",
"sync",
], optional = true }
xkbcommon = { version = "0.8.0", features = [
"wayland",
"x11",
], optional = true }
xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8", features = [
"x11rb-xcb",
"x11rb-client",
], package = "zed-xim", version = "0.4.0-zed", optional = true }
x11-clipboard = { version = "0.9.3", optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
flume = "0.11"
rand = "0.9"
windows = { version = "0.61", features = [
"Foundation_Numerics",
"Storage_Search",
"Storage_Streams",
@@ -268,39 +273,180 @@ features = [
"Win32_UI_Shell_Common",
"Win32_UI_Shell_PropertiesSystem",
"Win32_UI_WindowsAndMessaging",
]
] }
windows-core = "0.61"
windows-numerics = "0.2"
windows-registry = "0.5"
[dev-dependencies]
backtrace = "0.3"
collections = { package = "gpui_collections", version = "0.2.2", features = ["test-support"] }
env_logger = "0.11"
http_client = { package = "gpui_http_client", version = "0.2.2", features = ["test-support"] }
lyon = { version = "1.0", features = ["extra"] }
pretty_assertions = { version = "1.3.0", features = ["unstable"] }
rand = "0.9"
unicode-segmentation = "1.10"
util = { package = "gpui_util", version = "0.2.2", features = ["test-support"] }
[target.'cfg(target_os = "windows")'.build-dependencies]
embed-resource = "3.0"
windows-registry = "0.5"
[target.'cfg(target_os = "macos")'.build-dependencies]
bindgen = "0.71"
cbindgen = { version = "0.28.0", default-features = false }
naga = { version = "25.0", features = ["wgsl-in"] }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies]
naga = { version = "25.0", features = ["wgsl-in"] }
[patch.crates-io]
calloop = { git = "https://github.com/zed-industries/calloop" }
# ============================================================================
# Learn Examples - Educational examples for learning GPUI
# ============================================================================
[[example]]
name = "interactive_elements"
path = "examples/learn/interactive_elements.rs"
[[example]]
name = "creating_components"
path = "examples/learn/creating_components.rs"
[[example]]
name = "layout"
path = "examples/learn/layout.rs"
[[example]]
name = "styling"
path = "examples/learn/styling.rs"
[[example]]
name = "async_tasks"
path = "examples/learn/async_tasks.rs"
[[example]]
name = "custom_drawing"
path = "examples/learn/custom_drawing.rs"
[[example]]
name = "animation"
path = "examples/learn/animation.rs"
[[example]]
name = "text"
path = "examples/learn/text.rs"
# ============================================================================
# Bench Examples - Performance benchmarks
# ============================================================================
[[example]]
name = "data_table"
path = "examples/bench/data_table.rs"
[[example]]
name = "paths_bench"
path = "examples/bench/paths_bench.rs"
[[example]]
name = "pattern"
path = "examples/bench/pattern.rs"
[[example]]
name = "shadow"
path = "examples/bench/shadow.rs"
# ============================================================================
# Legacy Examples - Original examples (to be updated)
# ============================================================================
[[example]]
name = "focus_visible"
path = "examples/legacy/focus_visible.rs"
[[example]]
name = "gif_viewer"
path = "examples/legacy/gif_viewer.rs"
[[example]]
name = "gradient"
path = "examples/legacy/gradient.rs"
[[example]]
name = "hello_world"
path = "examples/legacy/hello_world.rs"
[[example]]
name = "image_loading"
path = "examples/legacy/image_loading.rs"
[[example]]
name = "input"
path = "examples/legacy/input.rs"
[[example]]
name = "layer_shell"
path = "examples/legacy/layer_shell.rs"
[[example]]
name = "on_window_close_quit"
path = "examples/legacy/on_window_close_quit.rs"
[[example]]
name = "opacity"
path = "examples/legacy/opacity.rs"
[[example]]
name = "scrollable"
path = "examples/legacy/scrollable.rs"
[[example]]
name = "svg"
path = "examples/legacy/svg/svg.rs"
[[example]]
name = "tab_stop"
path = "examples/legacy/tab_stop.rs"
[[example]]
name = "tree"
path = "examples/legacy/tree.rs"
[[example]]
name = "uniform_list"
path = "examples/legacy/uniform_list.rs"
[[example]]
name = "window"
path = "examples/legacy/window.rs"
[[example]]
name = "window_positioning"
path = "examples/legacy/window_positioning.rs"
[[example]]
name = "window_shadow"
path = "examples/legacy/window_shadow.rs"
[profile.dev]
split-debuginfo = "unpacked"
# https://github.com/rust-lang/cargo/issues/16104
incremental = false
codegen-units = 16
# mirror configuration for crates compiled for the build platform
# (without this cargo will compile ~400 crates twice)
[profile.dev.build-override]
codegen-units = 16
[profile.dev.package]
# proc-macros start
gpui_macros = { opt-level = 3 }
derive_refineable = { opt-level = 3 }
util_macros = { opt-level = 3 }
quote = { opt-level = 3 }
syn = { opt-level = 3 }
proc-macro2 = { opt-level = 3 }
# proc-macros end
taffy = { opt-level = 3 }
resvg = { opt-level = 3 }
collections = { codegen-units = 1 }
refineable = { codegen-units = 1 }
reqwest_client = { codegen-units = 1 }
[profile.release]
debug = "limited"
lto = "thin"
@@ -312,54 +458,19 @@ debug = "full"
lto = false
codegen-units = 16
[workspace.lints.rust]
[lints.rust]
unexpected_cfgs = { level = "allow" }
[workspace.lints.clippy]
[lints.clippy]
dbg_macro = "deny"
todo = "deny"
# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454
# Remove when the lint gets promoted to `suspicious`.
declare_interior_mutable_const = "deny"
redundant_clone = "deny"
disallowed_methods = "deny"
# We currently do not restrict any style rules
# as it slows down shipping code to Zed.
#
# Running ./script/clippy can take several minutes, and so it's
# common to skip that step and let CI do it. Any unexpected failures
# (which also take minutes to discover) thus require switching back
# to an old branch, manual fixing, and re-pushing.
#
# In the future we could improve this by either making sure
# Zed can surface clippy errors in diagnostics (in addition to the
# rust-analyzer errors), or by having CI fix style nits automatically.
style = { level = "allow", priority = -1 }
# Individual rules that have violations in the codebase:
type_complexity = "allow"
let_underscore_future = "allow"
# Motivation: We use `vec![a..b]` a lot when dealing with ranges in text, so
# warning on this rule produces a lot of noise.
single_range_in_vec_init = "allow"
# in Rust it can be very tedious to reduce argument count without
# running afoul of the borrow checker.
too_many_arguments = "allow"
# We often have large enum variants yet we rarely actually bother with splitting them up.
large_enum_variant = "allow"
# Boolean expressions can be hard to read, requiring only the minimal form gets in the way
nonminimal_bool = "allow"
[workspace.metadata.cargo-machete]
ignored = [
"bindgen",
"cbindgen",
"serde",
]
+74
View File
@@ -1,3 +1,77 @@
# gpui - Community Edition
A community fork of [GPUI](https://gpui.rs), Zed's GPU-accelerated UI framework.
## Usage
```toml
[dependencies]
gpui = { package = "gpui-ce", version = "0.3" }
# for test support...
[dev-dependencies]
gpui = { package = "gpui-ce", version = "0.3", features = ["test-support"] }
```
Then use `gpui::{import}` as normal.
---
todo: rewrite below...
# Welcome to GPUI!
GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
for Rust, designed to support a wide variety of applications.
Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
### Dependencies
GPUI has various system dependencies that it needs in order to work.
#### macOS
On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following:
- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account.
> Ensure you launch Xcode after installing, and install the macOS components, which is the default option.
- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/)
```sh
xcode-select --install
```
- Ensure that the Xcode command line tools are using your newly installed copy of Xcode:
```sh
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
```
## The Big Picture
GPUI offers three different [registers](<https://en.wikipedia.org/wiki/Register_(sociolinguistics)>) depending on your needs:
- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information.
- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering.
- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information.
Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework.
## Other Resources
In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications:
- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information.
- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`.
- An async executor that is integrated with the platform's event loop. See the `executor` module for more information.,
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
View File
-21
View File
@@ -1,21 +0,0 @@
[package]
name = "collections"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "Standard collection type re-exports used by Zed and GPUI"
[lints]
workspace = true
[lib]
path = "src/collections.rs"
doctest = false
[features]
test-support = []
[dependencies]
indexmap.workspace = true
rustc-hash.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-9
View File
@@ -1,9 +0,0 @@
pub type HashMap<K, V> = FxHashMap<K, V>;
pub type HashSet<T> = FxHashSet<T>;
pub type IndexMap<K, V> = indexmap::IndexMap<K, V, rustc_hash::FxBuildHasher>;
pub type IndexSet<T> = indexmap::IndexSet<T, rustc_hash::FxBuildHasher>;
pub use indexmap::Equivalent;
pub use rustc_hash::FxHasher;
pub use rustc_hash::{FxHashMap, FxHashSet};
pub use std::collections::*;
-332
View File
@@ -1,332 +0,0 @@
[package]
name = "gpui"
version = "0.2.2"
edition.workspace = true
authors = ["Nathan Sobo <nathan@zed.dev>"]
description = "Zed's GPU-accelerated UI framework"
repository = "https://github.com/zed-industries/zed"
publish = true
license = "Apache-2.0"
homepage = "https://gpui.rs"
readme = "README.md"
keywords = ["desktop", "gui", "immediate"]
categories = ["gui"]
[lints]
workspace = true
[features]
default = ["font-kit", "wayland", "x11", "windows-manifest"]
test-support = [
"leak-detection",
"collections/test-support",
"util/test-support",
"http_client/test-support",
"wayland",
"x11",
]
inspector = ["gpui_macros/inspector"]
leak-detection = ["backtrace"]
runtime_shaders = []
macos-blade = [
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"objc2",
"objc2-metal",
]
wayland = [
"bitflags",
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"ashpd/wayland",
"cosmic-text",
"font-kit",
"calloop-wayland-source",
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"filedescriptor",
"xkbcommon",
"open",
]
x11 = [
"blade-graphics",
"blade-macros",
"blade-util",
"bytemuck",
"ashpd",
"cosmic-text",
"font-kit",
"as-raw-xcb-connection",
"x11rb",
"xkbcommon",
"xim",
"x11-clipboard",
"filedescriptor",
"open",
"scap?/x11",
]
screen-capture = [
"scap",
]
windows-manifest = []
[lib]
path = "src/gpui.rs"
doctest = false
[dependencies]
anyhow.workspace = true
async-task = "4.7"
backtrace = { workspace = true, optional = true }
bitflags = { workspace = true, optional = true }
blade-graphics = { workspace = true, optional = true }
blade-macros = { workspace = true, optional = true }
blade-util = { workspace = true, optional = true }
bytemuck = { version = "1", optional = true }
collections.workspace = true
ctor.workspace = true
derive_more.workspace = true
etagere = "0.2"
futures.workspace = true
gpui_macros.workspace = true
http_client.workspace = true
image.workspace = true
inventory.workspace = true
itertools.workspace = true
log.workspace = true
num_cpus = "1.13"
parking = "2.0.0"
parking_lot.workspace = true
postage.workspace = true
profiling.workspace = true
rand.workspace = true
raw-window-handle = "0.6"
refineable.workspace = true
resvg = { version = "0.45.0", default-features = false, features = [
"text",
"system-fonts",
"memmap-fonts",
] }
usvg = { version = "0.45.0", default-features = false }
util_macros.workspace = true
schemars.workspace = true
seahash = "4.1"
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
slotmap.workspace = true
smallvec.workspace = true
smol.workspace = true
stacksafe.workspace = true
strum.workspace = true
sum_tree.workspace = true
taffy = "=0.9.0"
thiserror.workspace = true
util.workspace = true
uuid.workspace = true
waker-fn = "1.2.0"
lyon = "1.0"
libc.workspace = true
pin-project = "1.1.10"
circular-buffer.workspace = true
spin = "0.10.0"
[target.'cfg(target_os = "macos")'.dependencies]
block = "0.1"
cocoa.workspace = true
cocoa-foundation.workspace = true
core-foundation.workspace = true
core-foundation-sys.workspace = true
core-graphics = "0.24"
core-video.workspace = true
core-text = "21"
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", optional = true }
foreign-types = "0.5"
log.workspace = true
media.workspace = true
objc.workspace = true
objc2 = { version = "0.6", optional = true }
objc2-metal = { version = "0.3", optional = true }
mach2.workspace = true
#TODO: replace with "objc2"
metal.workspace = true
flume = "0.11"
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
pathfinder_geometry = "0.5"
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))'.dependencies]
scap = { workspace = true, optional = true }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
# Always used
flume = "0.11"
oo7 = { version = "0.5.0", default-features = false, features = [
"async-std",
"native_crypto",
] }
# Used in both windowing options
ashpd = { workspace = true, optional = true }
blade-graphics = { workspace = true, optional = true }
blade-macros = { workspace = true, optional = true }
blade-util = { workspace = true, optional = true }
bytemuck = { version = "1", optional = true }
cosmic-text = { version = "0.14.0", optional = true }
# WARNING: If you change this, you must also publish a new version of zed-font-kit to crates.io
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "110523127440aefb11ce0cf280ae7c5071337ec5", package = "zed-font-kit", version = "0.14.1-zed", features = [
"source-fontconfig-dlopen",
], optional = true }
calloop = { version = "0.14.3" }
filedescriptor = { version = "0.8.2", optional = true }
open = { version = "5.2.0", optional = true }
# Wayland
calloop-wayland-source = { version = "0.4.1", optional = true }
wayland-backend = { version = "0.3.3", features = [
"client_system",
"dlopen",
], optional = true }
wayland-client = { version = "0.31.2", optional = true }
wayland-cursor = { version = "0.31.1", optional = true }
wayland-protocols = { version = "0.31.2", features = [
"client",
"staging",
"unstable",
], optional = true }
wayland-protocols-plasma = { version = "0.2.0", features = [
"client",
], optional = true }
wayland-protocols-wlr = { version = "0.3.9", features = [
"client",
], optional = true }
# X11
as-raw-xcb-connection = { version = "1", optional = true }
x11rb = { version = "0.13.1", features = [
"allow-unsafe-code",
"xkb",
"randr",
"xinput",
"cursor",
"resource_manager",
"sync",
], optional = true }
xkbcommon = { version = "0.8.0", features = [
"wayland",
"x11",
], optional = true }
# WARNING: If you change this, you must also publish a new version of zed-xim to crates.io
xim = { git = "https://github.com/zed-industries/xim-rs.git", rev = "16f35a2c881b815a2b6cdfd6687988e84f8447d8" , features = [
"x11rb-xcb",
"x11rb-client",
], package = "zed-xim", version = "0.4.0-zed", optional = true }
x11-clipboard = { version = "0.9.3", optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
flume = "0.11"
rand.workspace = true
windows.workspace = true
windows-core.workspace = true
windows-numerics = "0.2"
windows-registry = "0.5"
[dev-dependencies]
backtrace.workspace = true
collections = { workspace = true, features = ["test-support"] }
env_logger.workspace = true
http_client = { workspace = true, features = ["test-support"] }
lyon = { version = "1.0", features = ["extra"] }
pretty_assertions.workspace = true
rand.workspace = true
reqwest_client = { workspace = true, features = ["test-support"] }
unicode-segmentation.workspace = true
util = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "windows")'.build-dependencies]
embed-resource = "3.0"
windows-registry = "0.5"
[target.'cfg(target_os = "macos")'.build-dependencies]
bindgen = "0.71"
cbindgen = { version = "0.28.0", default-features = false }
naga.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies]
naga.workspace = true
[[example]]
name = "hello_world"
path = "examples/hello_world.rs"
[[example]]
name = "image"
path = "examples/image/image.rs"
[[example]]
name = "input"
path = "examples/input.rs"
[[example]]
name = "on_window_close_quit"
path = "examples/on_window_close_quit.rs"
[[example]]
name = "opacity"
path = "examples/opacity.rs"
[[example]]
name = "pattern"
path = "examples/pattern.rs"
[[example]]
name = "set_menus"
path = "examples/set_menus.rs"
[[example]]
name = "shadow"
path = "examples/shadow.rs"
[[example]]
name = "svg"
path = "examples/svg/svg.rs"
[[example]]
name = "tab_stop"
path = "examples/tab_stop.rs"
[[example]]
name = "text"
path = "examples/text.rs"
[[example]]
name = "text_wrapper"
path = "examples/text_wrapper.rs"
[[example]]
name = "tree"
path = "examples/tree.rs"
[[example]]
name = "uniform_list"
path = "examples/uniform_list.rs"
[[example]]
name = "window_shadow"
path = "examples/window_shadow.rs"
[[example]]
name = "grid_layout"
path = "examples/grid_layout.rs"
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-66
View File
@@ -1,66 +0,0 @@
# Welcome to GPUI!
GPUI is a hybrid immediate and retained mode, GPU accelerated, UI framework
for Rust, designed to support a wide variety of applications.
## Getting Started
GPUI is still in active development as we work on the Zed code editor, and is still pre-1.0. There will often be breaking changes between versions. You'll also need to use the latest version of stable Rust and be on macOS or Linux. Add the following to your `Cargo.toml`:
```toml
gpui = { version = "*" }
```
- [Ownership and data flow](_ownership_and_data_flow)
Everything in GPUI starts with an `Application`. You can create one with `Application::new()`, and kick off your application by passing a callback to `Application::run()`. Inside this callback, you can create a new window with `App::open_window()`, and register your first root view. See [gpui.rs](https://www.gpui.rs/) for a complete example.
### Dependencies
GPUI has various system dependencies that it needs in order to work.
#### macOS
On macOS, GPUI uses Metal for rendering. In order to use Metal, you need to do the following:
- Install [Xcode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) from the macOS App Store, or from the [Apple Developer](https://developer.apple.com/download/all/) website. Note this requires a developer account.
> Ensure you launch Xcode after installing, and install the macOS components, which is the default option.
- Install [Xcode command line tools](https://developer.apple.com/xcode/resources/)
```sh
xcode-select --install
```
- Ensure that the Xcode command line tools are using your newly installed copy of Xcode:
```sh
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer
```
## The Big Picture
GPUI offers three different [registers](<https://en.wikipedia.org/wiki/Register_(sociolinguistics)>) depending on your needs:
- State management and communication with `Entity`'s. Whenever you need to store application state that communicates between different parts of your application, you'll want to use GPUI's entities. Entities are owned by GPUI and are only accessible through an owned smart pointer similar to an `Rc`. See the `app::context` module for more information.
- High level, declarative UI with views. All UI in GPUI starts with a view. A view is simply an `Entity` that can be rendered, by implementing the `Render` trait. At the start of each frame, GPUI will call this render method on the root view of a given window. Views build a tree of `elements`, lay them out and style them with a tailwind-style API, and then give them to GPUI to turn into pixels. See the `div` element for an all purpose swiss-army knife of rendering.
- Low level, imperative UI with Elements. Elements are the building blocks of UI in GPUI, and they provide a nice wrapper around an imperative API that provides as much flexibility and control as you need. Elements have total control over how they and their child elements are rendered and can be used for making efficient views into large lists, implement custom layouting for a code editor, and anything else you can think of. See the `element` module for more information.
Each of these registers has one or more corresponding contexts that can be accessed from all GPUI services. This context is your main interface to GPUI, and is used extensively throughout the framework.
## Other Resources
In addition to the systems above, GPUI provides a range of smaller services that are useful for building complex applications:
- Actions are user-defined structs that are used for converting keystrokes into logical operations in your UI. Use this for implementing keyboard shortcuts, such as cmd-q. See the `action` module for more information.
- Platform services, such as `quit the app` or `open a URL` are available as methods on the `app::App`.
- An async executor that is integrated with the platform's event loop. See the `executor` module for more information.,
- The `[gpui::test]` macro provides a convenient way to write tests for your GPUI applications. Tests also have their own kind of context, a `TestAppContext` which provides ways of simulating common platform input. See `app::test_context` and `test` modules for more details.
Currently, the best way to learn about these APIs is to read the Zed source code or drop a question in the [Zed Discord](https://zed.dev/community-links). We're working on improving the documentation, creating more examples, and will be publishing more guides to GPUI on our [blog](https://zed.dev/blog).
-121
View File
@@ -1,121 +0,0 @@
use std::time::Duration;
use anyhow::Result;
use gpui::{
Animation, AnimationExt as _, App, Application, AssetSource, Bounds, Context, SharedString,
Transformation, Window, WindowBounds, WindowOptions, bounce, div, ease_in_out, percentage,
prelude::*, px, size, svg,
};
struct Assets {}
impl AssetSource for Assets {
fn load(&self, path: &str) -> Result<Option<std::borrow::Cow<'static, [u8]>>> {
std::fs::read(path)
.map(Into::into)
.map_err(Into::into)
.map(Some)
}
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
Ok(std::fs::read_dir(path)?
.filter_map(|entry| {
Some(SharedString::from(
entry.ok()?.path().to_string_lossy().into_owned(),
))
})
.collect::<Vec<_>>())
}
}
const ARROW_CIRCLE_SVG: &str = concat!(
env!("CARGO_MANIFEST_DIR"),
"/examples/image/arrow_circle.svg"
);
struct AnimationExample {}
impl Render for AnimationExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.size_full()
.bg(gpui::white())
.text_color(gpui::black())
.justify_around()
.child(
div()
.flex()
.flex_col()
.size_full()
.justify_around()
.child(
div()
.id("content")
.flex()
.flex_col()
.h(px(150.))
.overflow_y_scroll()
.w_full()
.flex_1()
.justify_center()
.items_center()
.text_xl()
.gap_4()
.child("Hello Animation")
.child(
svg()
.size_20()
.overflow_hidden()
.path(ARROW_CIRCLE_SVG)
.text_color(gpui::black())
.with_animation(
"image_circle",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(bounce(ease_in_out)),
|svg, delta| {
svg.with_transformation(Transformation::rotate(
percentage(delta),
))
},
),
),
)
.child(
div()
.flex()
.h(px(64.))
.w_full()
.p_2()
.justify_center()
.items_center()
.border_t_1()
.border_color(gpui::black().opacity(0.1))
.bg(gpui::black().opacity(0.05))
.child("Other Panel"),
),
)
}
}
fn main() {
Application::new()
.with_assets(Assets {})
.run(|cx: &mut App| {
let options = WindowOptions {
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(300.), px(300.)),
cx,
))),
..Default::default()
};
cx.open_window(options, |_, cx| {
cx.activate(false);
cx.new(|_| AnimationExample {})
})
.unwrap();
});
}
-137
View File
@@ -1,137 +0,0 @@
use gpui::{
App, Application, Bounds, Context, Half, Hsla, Pixels, Point, Window, WindowBounds,
WindowOptions, div, prelude::*, px, rgb, size,
};
#[derive(Clone, Copy)]
struct DragInfo {
ix: usize,
color: Hsla,
position: Point<Pixels>,
}
impl DragInfo {
fn new(ix: usize, color: Hsla) -> Self {
Self {
ix,
color,
position: Point::default(),
}
}
fn position(mut self, pos: Point<Pixels>) -> Self {
self.position = pos;
self
}
}
impl Render for DragInfo {
fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
let size = gpui::size(px(120.), px(50.));
div()
.pl(self.position.x - size.width.half())
.pt(self.position.y - size.height.half())
.child(
div()
.flex()
.justify_center()
.items_center()
.w(size.width)
.h(size.height)
.bg(self.color.opacity(0.5))
.text_color(gpui::white())
.text_xs()
.shadow_md()
.child(format!("Item {}", self.ix)),
)
}
}
struct DragDrop {
drop_on: Option<DragInfo>,
}
impl DragDrop {
fn new() -> Self {
Self { drop_on: None }
}
}
impl Render for DragDrop {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let items = [gpui::blue(), gpui::red(), gpui::green()];
div()
.size_full()
.flex()
.flex_col()
.gap_5()
.bg(gpui::white())
.justify_center()
.items_center()
.text_color(rgb(0x333333))
.child(div().text_xl().text_center().child("Drop & Drop"))
.child(
div()
.w_full()
.mb_10()
.justify_center()
.flex()
.flex_row()
.gap_4()
.items_center()
.children(items.into_iter().enumerate().map(|(ix, color)| {
let drag_info = DragInfo::new(ix, color);
div()
.id(("item", ix))
.size_32()
.flex()
.justify_center()
.items_center()
.border_2()
.border_color(color)
.text_color(color)
.cursor_move()
.hover(|this| this.bg(color.opacity(0.2)))
.child(format!("Item ({})", ix))
.on_drag(drag_info, |info: &DragInfo, position, _, cx| {
cx.new(|_| info.position(position))
})
})),
)
.child(
div()
.id("drop-target")
.w_128()
.h_32()
.flex()
.justify_center()
.items_center()
.border_3()
.border_color(self.drop_on.map(|info| info.color).unwrap_or(gpui::black()))
.when_some(self.drop_on, |this, info| this.bg(info.color.opacity(0.5)))
.on_drop(cx.listener(|this, info: &DragInfo, _, _| {
this.drop_on = Some(*info);
}))
.child("Drop items here"),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| DragDrop::new()),
)
.unwrap();
cx.activate(true);
});
}
-80
View File
@@ -1,80 +0,0 @@
use gpui::{
App, Application, Bounds, Context, Hsla, Window, WindowBounds, WindowOptions, div, prelude::*,
px, rgb, size,
};
// https://en.wikipedia.org/wiki/Holy_grail_(web_design)
struct HolyGrailExample {}
impl Render for HolyGrailExample {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let block = |color: Hsla| {
div()
.size_full()
.bg(color)
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white())
.items_center()
};
div()
.gap_1()
.grid()
.bg(rgb(0x505050))
.size(px(500.0))
.shadow_lg()
.border_1()
.size_full()
.grid_cols(5)
.grid_rows(5)
.child(
block(gpui::white())
.row_span(1)
.col_span_full()
.child("Header"),
)
.child(
block(gpui::red())
.col_span(1)
.h_56()
.child("Table of contents"),
)
.child(
block(gpui::green())
.col_span(3)
.row_span(3)
.child("Content"),
)
.child(
block(gpui::blue())
.col_span(1)
.row_span(3)
.child("AD :(")
.text_color(gpui::white()),
)
.child(
block(gpui::black())
.row_span(1)
.col_span_full()
.text_color(gpui::white())
.child("Footer"),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HolyGrailExample {}),
)
.unwrap();
cx.activate(true);
});
}
-288
View File
@@ -1,288 +0,0 @@
use futures::FutureExt;
use gpui::{
App, AppContext, Application, Asset as _, AssetLogger, Bounds, ClickEvent, Context, ElementId,
Entity, ImageAssetLoader, ImageCache, ImageCacheProvider, KeyBinding, Menu, MenuItem,
RetainAllImageCache, SharedString, TitlebarOptions, Window, WindowBounds, WindowOptions,
actions, div, hash, image_cache, img, prelude::*, px, rgb, size,
};
use reqwest_client::ReqwestClient;
use std::{collections::HashMap, sync::Arc};
const IMAGES_IN_GALLERY: usize = 30;
struct ImageGallery {
image_key: String,
items_count: usize,
total_count: usize,
image_cache: Entity<RetainAllImageCache>,
}
impl ImageGallery {
fn on_next_image(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.image_cache
.update(cx, |image_cache, cx| image_cache.clear(window, cx));
let t = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis();
self.image_key = format!("{}", t);
self.total_count += self.items_count;
cx.notify();
}
}
impl Render for ImageGallery {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let image_url: SharedString =
format!("https://picsum.photos/400/200?t={}", self.image_key).into();
div()
.flex()
.flex_col()
.text_color(gpui::white())
.child("Manually managed image cache:")
.child(
div()
.image_cache(self.image_cache.clone())
.id("main")
.text_color(gpui::black())
.bg(rgb(0xE9E9E9))
.overflow_y_scroll()
.p_4()
.size_full()
.flex()
.flex_col()
.items_center()
.gap_2()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_between()
.child(format!(
"Example to show images and test memory usage (Rendered: {} images).",
self.total_count
))
.child(
div()
.id("btn")
.py_1()
.px_4()
.bg(gpui::black())
.hover(|this| this.opacity(0.8))
.text_color(gpui::white())
.text_center()
.w_40()
.child("Next Photos")
.on_click(cx.listener(Self::on_next_image)),
),
)
.child(
div()
.id("image-gallery")
.flex()
.flex_row()
.flex_wrap()
.gap_x_4()
.gap_y_2()
.justify_around()
.children(
(0..self.items_count)
.map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
),
),
)
.child(
"Automatically managed image cache:"
)
.child(image_cache(simple_lru_cache("lru-cache", IMAGES_IN_GALLERY)).child(
div()
.id("main")
.bg(rgb(0xE9E9E9))
.text_color(gpui::black())
.overflow_y_scroll()
.p_4()
.size_full()
.flex()
.flex_col()
.items_center()
.gap_2()
.child(
div()
.id("image-gallery")
.flex()
.flex_row()
.flex_wrap()
.gap_x_4()
.gap_y_2()
.justify_around()
.children(
(0..self.items_count)
.map(|ix| img(format!("{}-{}", image_url, ix)).size_20()),
),
)
))
}
}
fn simple_lru_cache(id: impl Into<ElementId>, max_items: usize) -> SimpleLruCacheProvider {
SimpleLruCacheProvider {
id: id.into(),
max_items,
}
}
struct SimpleLruCacheProvider {
id: ElementId,
max_items: usize,
}
impl ImageCacheProvider for SimpleLruCacheProvider {
fn provide(&mut self, window: &mut Window, cx: &mut App) -> gpui::AnyImageCache {
window
.with_global_id(self.id.clone(), |global_id, window| {
window.with_element_state::<Entity<SimpleLruCache>, _>(
global_id,
|lru_cache, _window| {
let mut lru_cache = lru_cache.unwrap_or_else(|| {
cx.new(|cx| SimpleLruCache::new(self.max_items, cx))
});
if lru_cache.read(cx).max_items != self.max_items {
lru_cache = cx.new(|cx| SimpleLruCache::new(self.max_items, cx));
}
(lru_cache.clone(), lru_cache)
},
)
})
.into()
}
}
struct SimpleLruCache {
max_items: usize,
usages: Vec<u64>,
cache: HashMap<u64, gpui::ImageCacheItem>,
}
impl SimpleLruCache {
fn new(max_items: usize, cx: &mut Context<Self>) -> Self {
cx.on_release(|simple_cache, cx| {
for (_, mut item) in std::mem::take(&mut simple_cache.cache) {
if let Some(Ok(image)) = item.get() {
cx.drop_image(image, None);
}
}
})
.detach();
Self {
max_items,
usages: Vec::with_capacity(max_items),
cache: HashMap::with_capacity(max_items),
}
}
}
impl ImageCache for SimpleLruCache {
fn load(
&mut self,
resource: &gpui::Resource,
window: &mut Window,
cx: &mut App,
) -> Option<Result<Arc<gpui::RenderImage>, gpui::ImageCacheError>> {
assert_eq!(self.usages.len(), self.cache.len());
assert!(self.cache.len() <= self.max_items);
let hash = hash(resource);
if let Some(item) = self.cache.get_mut(&hash) {
let current_ix = self
.usages
.iter()
.position(|item| *item == hash)
.expect("cache and usages must stay in sync");
self.usages.remove(current_ix);
self.usages.insert(0, hash);
return item.get();
}
let fut = AssetLogger::<ImageAssetLoader>::load(resource.clone(), cx);
let task = cx.background_executor().spawn(fut).shared();
if self.usages.len() == self.max_items {
let oldest = self.usages.pop().unwrap();
let mut image = self
.cache
.remove(&oldest)
.expect("cache and usages must be in sync");
if let Some(Ok(image)) = image.get() {
cx.drop_image(image, Some(window));
}
}
self.cache
.insert(hash, gpui::ImageCacheItem::Loading(task.clone()));
self.usages.insert(0, hash);
let entity = window.current_view();
window
.spawn(cx, {
async move |cx| {
_ = task.await;
cx.on_next_frame(move |_, cx| {
cx.notify(entity);
});
}
})
.detach();
None
}
}
actions!(image, [Quit]);
fn main() {
env_logger::init();
Application::new().run(move |cx: &mut App| {
let http_client = ReqwestClient::user_agent("gpui example").unwrap();
cx.set_http_client(Arc::new(http_client));
cx.activate(true);
cx.on_action(|_: &Quit, cx| cx.quit());
cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]);
cx.set_menus(vec![Menu {
name: "Image Gallery".into(),
items: vec![MenuItem::action("Quit", Quit)],
}]);
let window_options = WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some(SharedString::from("Image Gallery")),
appears_transparent: false,
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(Bounds::centered(
None,
size(px(1100.), px(860.)),
cx,
))),
..Default::default()
};
cx.open_window(window_options, |_, cx| {
cx.new(|ctx| ImageGallery {
image_key: "".into(),
items_count: IMAGES_IN_GALLERY,
total_count: 0,
image_cache: RetainAllImageCache::new(ctx),
})
})
.unwrap();
});
}
-35
View File
@@ -1,35 +0,0 @@
use gpui::{App, Application, Context, Entity, EventEmitter, prelude::*};
struct Counter {
count: usize,
}
struct Change {
increment: usize,
}
impl EventEmitter<Change> for Counter {}
fn main() {
Application::new().run(|cx: &mut App| {
let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
let subscriber = cx.new(|cx: &mut Context<Counter>| {
cx.subscribe(&counter, |subscriber, _emitter, event, _cx| {
subscriber.count += event.increment * 2;
})
.detach();
Counter {
count: counter.read(cx).count * 2,
}
});
counter.update(cx, |counter, cx| {
counter.count += 2;
cx.notify();
cx.emit(Change { increment: 2 });
});
assert_eq!(subscriber.read(cx).count, 4);
});
}
-463
View File
@@ -1,463 +0,0 @@
use gpui::{
Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder,
PathStyle, Pixels, Point, Render, StrokeOptions, Window, WindowOptions, canvas, div,
linear_color_stop, linear_gradient, point, prelude::*, px, quad, rgb, size,
};
struct PaintingViewer {
default_lines: Vec<(Path<Pixels>, Background)>,
background_quads: Vec<(Bounds<Pixels>, Background)>,
lines: Vec<Vec<Point<Pixels>>>,
start: Point<Pixels>,
dashed: bool,
_painting: bool,
}
impl PaintingViewer {
fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
let mut lines = vec![];
// Black squares beneath transparent paths.
let background_quads = vec![
(
Bounds {
origin: point(px(70.), px(70.)),
size: size(px(40.), px(40.)),
},
gpui::black().into(),
),
(
Bounds {
origin: point(px(170.), px(70.)),
size: size(px(40.), px(40.)),
},
gpui::black().into(),
),
(
Bounds {
origin: point(px(270.), px(70.)),
size: size(px(40.), px(40.)),
},
gpui::black().into(),
),
(
Bounds {
origin: point(px(370.), px(70.)),
size: size(px(40.), px(40.)),
},
gpui::black().into(),
),
(
Bounds {
origin: point(px(450.), px(50.)),
size: size(px(80.), px(80.)),
},
gpui::black().into(),
),
];
// 50% opaque red path that extends across black quad.
let mut builder = PathBuilder::fill();
builder.move_to(point(px(50.), px(50.)));
builder.line_to(point(px(130.), px(50.)));
builder.line_to(point(px(130.), px(130.)));
builder.line_to(point(px(50.), px(130.)));
builder.close();
let path = builder.build().unwrap();
let mut red = rgb(0xFF0000);
red.a = 0.5;
lines.push((path, red.into()));
// 50% opaque blue path that extends across black quad.
let mut builder = PathBuilder::fill();
builder.move_to(point(px(150.), px(50.)));
builder.line_to(point(px(230.), px(50.)));
builder.line_to(point(px(230.), px(130.)));
builder.line_to(point(px(150.), px(130.)));
builder.close();
let path = builder.build().unwrap();
let mut blue = rgb(0x0000FF);
blue.a = 0.5;
lines.push((path, blue.into()));
// 50% opaque green path that extends across black quad.
let mut builder = PathBuilder::fill();
builder.move_to(point(px(250.), px(50.)));
builder.line_to(point(px(330.), px(50.)));
builder.line_to(point(px(330.), px(130.)));
builder.line_to(point(px(250.), px(130.)));
builder.close();
let path = builder.build().unwrap();
let mut green = rgb(0x00FF00);
green.a = 0.5;
lines.push((path, green.into()));
// 50% opaque black path that extends across black quad.
let mut builder = PathBuilder::fill();
builder.move_to(point(px(350.), px(50.)));
builder.line_to(point(px(430.), px(50.)));
builder.line_to(point(px(430.), px(130.)));
builder.line_to(point(px(350.), px(130.)));
builder.close();
let path = builder.build().unwrap();
let mut black = rgb(0x000000);
black.a = 0.5;
lines.push((path, black.into()));
// Two 50% opaque red circles overlapping - center should be darker red
let mut builder = PathBuilder::fill();
let center = point(px(530.), px(85.));
let radius = px(30.);
builder.move_to(point(center.x + radius, center.y));
builder.arc_to(
point(radius, radius),
px(0.),
false,
false,
point(center.x - radius, center.y),
);
builder.arc_to(
point(radius, radius),
px(0.),
false,
false,
point(center.x + radius, center.y),
);
builder.close();
let path = builder.build().unwrap();
let mut red1 = rgb(0xFF0000);
red1.a = 0.5;
lines.push((path, red1.into()));
let mut builder = PathBuilder::fill();
let center = point(px(570.), px(85.));
let radius = px(30.);
builder.move_to(point(center.x + radius, center.y));
builder.arc_to(
point(radius, radius),
px(0.),
false,
false,
point(center.x - radius, center.y),
);
builder.arc_to(
point(radius, radius),
px(0.),
false,
false,
point(center.x + radius, center.y),
);
builder.close();
let path = builder.build().unwrap();
let mut red2 = rgb(0xFF0000);
red2.a = 0.5;
lines.push((path, red2.into()));
// draw a Rust logo
let mut builder = lyon::path::Path::svg_builder();
lyon::extra::rust_logo::build_logo_path(&mut builder);
// move down the Path
let mut builder: PathBuilder = builder.into();
builder.translate(point(px(10.), px(200.)));
builder.scale(0.9);
let path = builder.build().unwrap();
lines.push((path, gpui::black().into()));
// draw a lightening bolt ⚡
let mut builder = PathBuilder::fill();
builder.add_polygon(
&[
point(px(150.), px(300.)),
point(px(200.), px(225.)),
point(px(200.), px(275.)),
point(px(250.), px(200.)),
],
false,
);
let path = builder.build().unwrap();
lines.push((path, rgb(0x1d4ed8).into()));
// draw a ⭐
let mut builder = PathBuilder::fill();
builder.move_to(point(px(350.), px(200.)));
builder.line_to(point(px(370.), px(260.)));
builder.line_to(point(px(430.), px(260.)));
builder.line_to(point(px(380.), px(300.)));
builder.line_to(point(px(400.), px(360.)));
builder.line_to(point(px(350.), px(320.)));
builder.line_to(point(px(300.), px(360.)));
builder.line_to(point(px(320.), px(300.)));
builder.line_to(point(px(270.), px(260.)));
builder.line_to(point(px(330.), px(260.)));
builder.line_to(point(px(350.), px(200.)));
let path = builder.build().unwrap();
lines.push((
path,
linear_gradient(
180.,
linear_color_stop(rgb(0xFACC15), 0.7),
linear_color_stop(rgb(0xD56D0C), 1.),
)
.color_space(ColorSpace::Oklab),
));
// draw linear gradient
let square_bounds = Bounds {
origin: point(px(450.), px(200.)),
size: size(px(200.), px(80.)),
};
let height = square_bounds.size.height;
let horizontal_offset = height;
let vertical_offset = px(30.);
let mut builder = PathBuilder::fill();
builder.move_to(square_bounds.bottom_left());
builder.curve_to(
square_bounds.origin + point(horizontal_offset, vertical_offset),
square_bounds.origin + point(px(0.0), vertical_offset),
);
builder.line_to(square_bounds.top_right() + point(-horizontal_offset, vertical_offset));
builder.curve_to(
square_bounds.bottom_right(),
square_bounds.top_right() + point(px(0.0), vertical_offset),
);
builder.line_to(square_bounds.bottom_left());
let path = builder.build().unwrap();
lines.push((
path,
linear_gradient(
180.,
linear_color_stop(gpui::blue(), 0.4),
linear_color_stop(gpui::red(), 1.),
),
));
// draw a pie chart
let center = point(px(96.), px(96.));
let pie_center = point(px(775.), px(255.));
let segments = [
(
point(px(871.), px(255.)),
point(px(747.), px(163.)),
rgb(0x1374e9),
),
(
point(px(747.), px(163.)),
point(px(679.), px(263.)),
rgb(0xe13527),
),
(
point(px(679.), px(263.)),
point(px(754.), px(349.)),
rgb(0x0751ce),
),
(
point(px(754.), px(349.)),
point(px(854.), px(310.)),
rgb(0x209742),
),
(
point(px(854.), px(310.)),
point(px(871.), px(255.)),
rgb(0xfbc10a),
),
];
for (start, end, color) in segments {
let mut builder = PathBuilder::fill();
builder.move_to(start);
builder.arc_to(center, px(0.), false, false, end);
builder.line_to(pie_center);
builder.close();
let path = builder.build().unwrap();
lines.push((path, color.into()));
}
// draw a wave
let options = StrokeOptions::default()
.with_line_width(1.)
.with_line_join(lyon::path::LineJoin::Bevel);
let mut builder = PathBuilder::stroke(px(1.)).with_style(PathStyle::Stroke(options));
builder.move_to(point(px(40.), px(420.)));
for i in 1..50 {
builder.line_to(point(
px(40.0 + i as f32 * 10.0),
px(420.0 + (i as f32 * 10.0).sin() * 40.0),
));
}
let path = builder.build().unwrap();
lines.push((path, gpui::green().into()));
Self {
default_lines: lines.clone(),
background_quads,
lines: vec![],
start: point(px(0.), px(0.)),
dashed: false,
_painting: false,
}
}
fn clear(&mut self, cx: &mut Context<Self>) {
self.lines.clear();
cx.notify();
}
}
fn button(
text: &str,
cx: &mut Context<PaintingViewer>,
on_click: impl Fn(&mut PaintingViewer, &mut Context<PaintingViewer>) + 'static,
) -> impl IntoElement {
div()
.id(text.to_string())
.child(text.to_string())
.bg(gpui::black())
.text_color(gpui::white())
.active(|this| this.opacity(0.8))
.flex()
.px_3()
.py_1()
.on_click(cx.listener(move |this, _, _, cx| on_click(this, cx)))
}
impl Render for PaintingViewer {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let default_lines = self.default_lines.clone();
let background_quads = self.background_quads.clone();
let lines = self.lines.clone();
let dashed = self.dashed;
div()
.bg(gpui::white())
.size_full()
.p_4()
.flex()
.flex_col()
.child(
div()
.flex()
.gap_2()
.justify_between()
.items_center()
.child("Mouse down any point and drag to draw lines (Hold on shift key to draw straight lines)")
.child(
div()
.flex()
.gap_x_2()
.child(button(
if dashed { "Solid" } else { "Dashed" },
cx,
move |this, _| this.dashed = !dashed,
))
.child(button("Clear", cx, |this, cx| this.clear(cx))),
),
)
.child(
div()
.size_full()
.child(
canvas(
move |_, _, _| {},
move |_, _, window, _| {
// First draw background quads
for (bounds, color) in background_quads.iter() {
window.paint_quad(quad(
*bounds,
px(0.),
*color,
px(0.),
gpui::transparent_black(),
Default::default(),
));
}
// Then draw the default paths on top
for (path, color) in default_lines {
window.paint_path(path, color);
}
for points in lines {
if points.len() < 2 {
continue;
}
let mut builder = PathBuilder::stroke(px(1.));
if dashed {
builder = builder.dash_array(&[px(4.), px(2.)]);
}
for (i, p) in points.into_iter().enumerate() {
if i == 0 {
builder.move_to(p);
} else {
builder.line_to(p);
}
}
if let Ok(path) = builder.build() {
window.paint_path(path, gpui::black());
}
}
},
)
.size_full(),
)
.on_mouse_down(
gpui::MouseButton::Left,
cx.listener(|this, ev: &MouseDownEvent, _, _| {
this._painting = true;
this.start = ev.position;
let path = vec![ev.position];
this.lines.push(path);
}),
)
.on_mouse_move(cx.listener(|this, ev: &gpui::MouseMoveEvent, _, cx| {
if !this._painting {
return;
}
let is_shifted = ev.modifiers.shift;
let mut pos = ev.position;
// When holding shift, draw a straight line
if is_shifted {
let dx = pos.x - this.start.x;
let dy = pos.y - this.start.y;
if dx.abs() > dy.abs() {
pos.y = this.start.y;
} else {
pos.x = this.start.x;
}
}
if let Some(path) = this.lines.last_mut() {
path.push(pos);
}
cx.notify();
}))
.on_mouse_up(
gpui::MouseButton::Left,
cx.listener(|this, _, _, _| {
this._painting = false;
}),
),
)
}
}
fn main() {
Application::new().run(|cx| {
cx.open_window(
WindowOptions {
focus: true,
..Default::default()
},
|window, cx| cx.new(|cx| PaintingViewer::new(window, cx)),
)
.unwrap();
cx.on_window_closed(|cx| {
cx.quit();
})
.detach();
cx.activate(true);
});
}
-107
View File
@@ -1,107 +0,0 @@
use gpui::{
App, Application, Context, Global, Menu, MenuItem, SharedString, SystemMenuType, Window,
WindowOptions, actions, div, prelude::*, rgb,
};
struct SetMenus;
impl Render for SetMenus {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.bg(rgb(0x2e7d32))
.size_full()
.justify_center()
.items_center()
.text_xl()
.text_color(rgb(0xffffff))
.child("Set Menus Example")
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.set_global(AppState::new());
// Bring the menu bar to the foreground (so you can see the menu bar)
cx.activate(true);
// Register the `quit` function so it can be referenced by the `MenuItem::action` in the menu bar
cx.on_action(quit);
cx.on_action(toggle_check);
// Add menu items
set_app_menus(cx);
cx.open_window(WindowOptions::default(), |_, cx| cx.new(|_| SetMenus {}))
.unwrap();
});
}
#[derive(PartialEq)]
enum ViewMode {
List,
Grid,
}
impl ViewMode {
fn toggle(&mut self) {
*self = match self {
ViewMode::List => ViewMode::Grid,
ViewMode::Grid => ViewMode::List,
}
}
}
impl Into<SharedString> for ViewMode {
fn into(self) -> SharedString {
match self {
ViewMode::List => "List",
ViewMode::Grid => "Grid",
}
.into()
}
}
struct AppState {
view_mode: ViewMode,
}
impl AppState {
fn new() -> Self {
Self {
view_mode: ViewMode::List,
}
}
}
impl Global for AppState {}
fn set_app_menus(cx: &mut App) {
let app_state = cx.global::<AppState>();
cx.set_menus(vec![Menu {
name: "set_menus".into(),
items: vec![
MenuItem::os_submenu("Services", SystemMenuType::Services),
MenuItem::separator(),
MenuItem::action(ViewMode::List, ToggleCheck)
.checked(app_state.view_mode == ViewMode::List),
MenuItem::action(ViewMode::Grid, ToggleCheck)
.checked(app_state.view_mode == ViewMode::Grid),
MenuItem::separator(),
MenuItem::action("Quit", Quit),
],
}]);
}
// Associate actions using the `actions!` macro (or `Action` derive macro)
actions!(set_menus, [Quit, ToggleCheck]);
// Define the quit function that is registered with the App
fn quit(_: &Quit, cx: &mut App) {
println!("Gracefully quitting the application . . .");
cx.quit();
}
fn toggle_check(_: &ToggleCheck, cx: &mut App) {
let app_state = cx.global_mut::<AppState>();
app_state.view_mode.toggle();
set_app_menus(cx);
}
-333
View File
@@ -1,333 +0,0 @@
use std::{
ops::{Deref, DerefMut},
sync::Arc,
};
use gpui::{
AbsoluteLength, App, Application, Context, DefiniteLength, ElementId, Global, Hsla, Menu,
SharedString, TextStyle, TitlebarOptions, Window, WindowBounds, WindowOptions, bounds,
colors::DefaultColors, div, point, prelude::*, px, relative, rgb, size,
};
use std::iter;
#[derive(Clone, Debug)]
pub struct TextContext {
font_size: f32,
line_height: f32,
type_scale: f32,
}
impl Default for TextContext {
fn default() -> Self {
TextContext {
font_size: 16.0,
line_height: 1.3,
type_scale: 1.33,
}
}
}
impl TextContext {
pub fn get_global(cx: &App) -> &Arc<TextContext> {
&cx.global::<GlobalTextContext>().0
}
}
#[derive(Clone, Debug)]
pub struct GlobalTextContext(pub Arc<TextContext>);
impl Deref for GlobalTextContext {
type Target = Arc<TextContext>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for GlobalTextContext {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Global for GlobalTextContext {}
pub trait ActiveTextContext {
fn text_context(&self) -> &Arc<TextContext>;
}
impl ActiveTextContext for App {
fn text_context(&self) -> &Arc<TextContext> {
&self.global::<GlobalTextContext>().0
}
}
#[derive(Clone, PartialEq)]
pub struct SpecimenTheme {
pub bg: Hsla,
pub fg: Hsla,
}
impl Default for SpecimenTheme {
fn default() -> Self {
Self {
bg: gpui::white(),
fg: gpui::black(),
}
}
}
impl SpecimenTheme {
pub fn invert(&self) -> Self {
Self {
bg: self.fg,
fg: self.bg,
}
}
}
#[derive(Debug, Clone, PartialEq, IntoElement)]
struct Specimen {
id: ElementId,
scale: f32,
text_style: Option<TextStyle>,
string: SharedString,
invert: bool,
}
impl Specimen {
pub fn new(id: usize) -> Self {
let string = SharedString::new_static("The quick brown fox jumps over the lazy dog");
let id_string = format!("specimen-{}", id);
let id = ElementId::Name(id_string.into());
Self {
id,
scale: 1.0,
text_style: None,
string,
invert: false,
}
}
pub fn invert(mut self) -> Self {
self.invert = !self.invert;
self
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
}
impl RenderOnce for Specimen {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let rem_size = window.rem_size();
let scale = self.scale;
let global_style = cx.text_context();
let style_override = self.text_style;
let mut font_size = global_style.font_size;
let mut line_height = global_style.line_height;
if let Some(style_override) = style_override {
font_size = style_override.font_size.to_pixels(rem_size).into();
line_height = match style_override.line_height {
DefiniteLength::Absolute(absolute_len) => match absolute_len {
AbsoluteLength::Rems(absolute_len) => absolute_len.to_pixels(rem_size).into(),
AbsoluteLength::Pixels(absolute_len) => absolute_len.into(),
},
DefiniteLength::Fraction(value) => value,
};
}
let mut theme = SpecimenTheme::default();
if self.invert {
theme = theme.invert();
}
div()
.id(self.id)
.bg(theme.bg)
.text_color(theme.fg)
.text_size(px(font_size * scale))
.line_height(relative(line_height))
.p(px(10.0))
.child(self.string)
}
}
#[derive(Debug, Clone, PartialEq, IntoElement)]
struct CharacterGrid {
scale: f32,
invert: bool,
text_style: Option<TextStyle>,
}
impl CharacterGrid {
pub fn new() -> Self {
Self {
scale: 1.0,
invert: false,
text_style: None,
}
}
pub fn scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
}
impl RenderOnce for CharacterGrid {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let mut theme = SpecimenTheme::default();
if self.invert {
theme = theme.invert();
}
let characters = vec![
"1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y",
"Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "p", "q",
"r", "s", "t", "u", "v", "w", "x", "y", "z", "", "ſ", "ß", "ð", "Þ", "þ", "α", "β",
"Γ", "γ", "Δ", "δ", "η", "θ", "ι", "κ", "Λ", "λ", "μ", "ν", "ξ", "π", "τ", "υ", "φ",
"χ", "ψ", "", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "", "µ",
"", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
];
let columns = 11;
let rows = characters.len().div_ceil(columns);
let grid_rows = (0..rows).map(|row_idx| {
let start_idx = row_idx * columns;
let end_idx = (start_idx + columns).min(characters.len());
div()
.w_full()
.flex()
.flex_row()
.children((start_idx..end_idx).map(|i| {
div()
.text_center()
.size(px(62.))
.bg(theme.bg)
.text_color(theme.fg)
.text_size(px(24.0))
.line_height(relative(1.0))
.child(characters[i])
}))
.when(end_idx - start_idx < columns, |d| {
d.children(
iter::repeat_with(|| div().flex_1()).take(columns - (end_idx - start_idx)),
)
})
});
div().p_4().gap_2().flex().flex_col().children(grid_rows)
}
}
struct TextExample {
next_id: usize,
}
impl TextExample {
fn next_id(&mut self) -> usize {
self.next_id += 1;
self.next_id
}
}
impl Render for TextExample {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let tcx = cx.text_context();
let colors = cx.default_colors().clone();
let type_scale = tcx.type_scale;
let step_down_2 = 1.0 / (type_scale * type_scale);
let step_down_1 = 1.0 / type_scale;
let base = 1.0;
let step_up_1 = base * type_scale;
let step_up_2 = step_up_1 * type_scale;
let step_up_3 = step_up_2 * type_scale;
let step_up_4 = step_up_3 * type_scale;
let step_up_5 = step_up_4 * type_scale;
let step_up_6 = step_up_5 * type_scale;
div()
.size_full()
.child(
div()
.id("text-example")
.overflow_y_scroll()
.overflow_x_hidden()
.bg(rgb(0xffffff))
.size_full()
.child(div().child(CharacterGrid::new().scale(base)))
.child(
div()
.child(Specimen::new(self.next_id()).scale(step_down_2))
.child(Specimen::new(self.next_id()).scale(step_down_2).invert())
.child(Specimen::new(self.next_id()).scale(step_down_1))
.child(Specimen::new(self.next_id()).scale(step_down_1).invert())
.child(Specimen::new(self.next_id()).scale(base))
.child(Specimen::new(self.next_id()).scale(base).invert())
.child(Specimen::new(self.next_id()).scale(step_up_1))
.child(Specimen::new(self.next_id()).scale(step_up_1).invert())
.child(Specimen::new(self.next_id()).scale(step_up_2))
.child(Specimen::new(self.next_id()).scale(step_up_2).invert())
.child(Specimen::new(self.next_id()).scale(step_up_3))
.child(Specimen::new(self.next_id()).scale(step_up_3).invert())
.child(Specimen::new(self.next_id()).scale(step_up_4))
.child(Specimen::new(self.next_id()).scale(step_up_4).invert())
.child(Specimen::new(self.next_id()).scale(step_up_5))
.child(Specimen::new(self.next_id()).scale(step_up_5).invert())
.child(Specimen::new(self.next_id()).scale(step_up_6))
.child(Specimen::new(self.next_id()).scale(step_up_6).invert()),
),
)
.child(div().w(px(240.)).h_full().bg(colors.container))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
cx.set_menus(vec![Menu {
name: "GPUI Typography".into(),
items: vec![],
}]);
cx.init_colors();
cx.set_global(GlobalTextContext(Arc::new(TextContext::default())));
let window = cx
.open_window(
WindowOptions {
titlebar: Some(TitlebarOptions {
title: Some("GPUI Typography".into()),
..Default::default()
}),
window_bounds: Some(WindowBounds::Windowed(bounds(
point(px(0.0), px(0.0)),
size(px(920.), px(720.)),
))),
..Default::default()
},
|_window, cx| cx.new(|_cx| TextExample { next_id: 0 }),
)
.unwrap();
window
.update(cx, |_view, _window, cx| {
cx.activate(true);
})
.unwrap();
});
}
-96
View File
@@ -1,96 +0,0 @@
use gpui::{
App, Application, Bounds, Context, FontStyle, FontWeight, StyledText, Window, WindowBounds,
WindowOptions, div, prelude::*, px, size,
};
struct HelloWorld {}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.bg(gpui::white())
.flex()
.flex_col()
.gap_2()
.p_4()
.size_full()
.child(div().child("Text left"))
.child(div().text_center().child("Text center"))
.child(div().text_right().child("Text right"))
.child(div().text_decoration_1().child("Text left (underline)"))
.child(
div()
.text_center()
.text_decoration_1()
.child("Text center (underline)"),
)
.child(
div()
.text_right()
.text_decoration_1()
.child("Text right (underline)"),
)
.child(div().line_through().child("Text left (line_through)"))
.child(
div()
.text_center()
.line_through()
.child("Text center (line_through)"),
)
.child(
div()
.text_right()
.line_through()
.child("Text right (line_through)"),
)
.child(
div()
.flex()
.gap_2()
.justify_between()
.child(
div()
.w(px(400.))
.border_1()
.border_color(gpui::blue())
.p_1()
.whitespace_nowrap()
.overflow_hidden()
.text_center()
.child("A long non-wrapping text align center"),
)
.child(
div()
.w_32()
.border_1()
.border_color(gpui::blue())
.p_1()
.whitespace_nowrap()
.overflow_hidden()
.text_right()
.child("100%"),
),
)
.child(div().flex().gap_2().justify_between().child(
StyledText::new("ABCD").with_highlights([
(0..1, FontWeight::EXTRA_BOLD.into()),
(2..3, FontStyle::Italic.into()),
]),
))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HelloWorld {}),
)
.unwrap();
cx.activate(true);
});
}
-123
View File
@@ -1,123 +0,0 @@
use gpui::{
App, Application, Bounds, Context, TextOverflow, Window, WindowBounds, WindowOptions, div,
prelude::*, px, size,
};
struct HelloWorld {}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
let text = "The longest word 你好世界这段是中文,こんにちはこの段落は日本語です in any of the major \
English language dictionaries is pneumonoultramicroscopicsilicovolcanoconiosis, a word that \
refers to a lung disease contracted from the inhalation of very fine silica particles, \
a url https://github.com/zed-industries/zed/pull/35724?query=foo&bar=2, \
specifically from a volcano; medically, it is the same as silicosis.";
div()
.id("page")
.size_full()
.flex()
.flex_col()
.p_2()
.gap_2()
.bg(gpui::white())
.child(
div()
.flex()
.flex_row()
.flex_shrink_0()
.gap_2()
.child(
div()
.flex()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.child("longer text in flex 1"),
)
.child(
div()
.flex()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.child("short flex"),
)
.child(
div()
.overflow_hidden()
.border_1()
.border_color(gpui::red())
.text_ellipsis()
.w_full()
.child("A short text in normal div"),
),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.truncate()
.border_1()
.border_color(gpui::blue())
.child("ELLIPSIS: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_ellipsis()
.line_clamp(2)
.border_1()
.border_color(gpui::blue())
.child("ELLIPSIS 2 lines: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_overflow(TextOverflow::Truncate("".into()))
.border_1()
.border_color(gpui::green())
.child("TRUNCATE: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.overflow_hidden()
.text_overflow(TextOverflow::Truncate("".into()))
.line_clamp(3)
.border_1()
.border_color(gpui::green())
.child("TRUNCATE 3 lines: ".to_owned() + text),
)
.child(
div()
.flex_shrink_0()
.text_xl()
.whitespace_nowrap()
.overflow_hidden()
.border_1()
.border_color(gpui::black())
.child("NOWRAP: ".to_owned() + text),
)
.child(div().text_xl().w_full().child(text))
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(800.0), px(600.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HelloWorld {}),
)
.unwrap();
cx.activate(true);
});
}
-140
View File
@@ -1,140 +0,0 @@
//! In GPUI, every model or view in the application is actually owned by a single top-level object called the `App`. When a new entity or view is created (referred to collectively as _entities_), the application is given ownership of their state to enable their participation in a variety of app services and interaction with other entities.
//!
//! To illustrate, consider the trivial app below. We start the app by calling `run` with a callback, which is passed a reference to the `App` that owns all the state for the application. This `App` is our gateway to all application-level services, such as opening windows, presenting dialogs, etc. It also has an `insert_entity` method, which is called below to create an entity and give ownership of it to the application.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let _counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! // ...
//! });
//! ```
//!
//! The call to `new_entity` returns an _entity handle_, which carries a type parameter based on the type of object it references. By itself, this `Entity<Counter>` handle doesn't provide access to the entity's state. It's merely an inert identifier plus a compile-time type tag, and it maintains a reference counted pointer to the underlying `Counter` object that is owned by the app.
//!
//! Much like an `Rc` from the Rust standard library, this reference count is incremented when the handle is cloned and decremented when it is dropped to enable shared ownership over the underlying model, but unlike an `Rc` it only provides access to the model's state when a reference to an `App` is available. The handle doesn't truly _own_ the state, but it can be used to access the state from its true owner, the `App`. Stripping away some of the setup code for brevity:
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Context, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! // Call `update` to access the model's state.
//! counter.update(cx, |counter: &mut Counter, _cx: &mut Context<Counter>| {
//! counter.count += 1;
//! });
//! });
//! ```
//!
//! To update the counter, we call `update` on the handle, passing the context reference and a callback. The callback is yielded a mutable reference to the counter, which can be used to manipulate state.
//!
//! The callback is also provided a second `Context<Counter>` reference. This reference is similar to the `App` reference provided to the `run` callback. A `Context` is actually a wrapper around the `App`, including some additional data to indicate which particular entity it is tied to; in this case the counter.
//!
//! In addition to the application-level services provided by `App`, a `Context` provides access to entity-level services. For example, it can be used it to inform observers of this entity that its state has changed. Let's add that to our example, by calling `cx.notify()`.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//! counter.update(cx, |counter, cx| {
//! counter.count += 1;
//! cx.notify(); // Notify observers
//! });
//! });
//! ```
//!
//! Next, these notifications need to be observed and reacted to. Before updating the counter, we'll construct a second counter that observes it. Whenever the first counter changes, twice its count is assigned to the second counter. Note how `observe` is called on the `Context` belonging to our second counter to arrange for it to be notified whenever the first counter notifies. The call to `observe` returns a `Subscription`, which is `detach`ed to preserve this behavior for as long as both counters exist. We could also store this subscription and drop it at a time of our choosing to cancel this behavior.
//!
//! The `observe` callback is passed a mutable reference to the observer and a _handle_ to the observed counter, whose state we access with the `read` method.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Entity, prelude::*};
//! # struct Counter {
//! # count: usize,
//! # }
//! Application::new().run(|cx: &mut App| {
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//!
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
//! // Note we can set up the callback before the Counter is even created!
//! cx.observe(
//! &first_counter,
//! |second: &mut Counter, first: Entity<Counter>, cx| {
//! second.count = first.read(cx).count * 2;
//! },
//! )
//! .detach();
//!
//! Counter { count: 0 }
//! });
//!
//! first_counter.update(cx, |counter, cx| {
//! counter.count += 1;
//! cx.notify();
//! });
//!
//! assert_eq!(second_counter.read(cx).count, 2);
//! });
//! ```
//!
//! After updating the first counter, it can be noted that the observing counter's state is maintained according to our subscription.
//!
//! In addition to `observe` and `notify`, which indicate that an entity's state has changed, GPUI also offers `subscribe` and `emit`, which enables entities to emit typed events. To opt into this system, the emitting object must implement the `EventEmitter` trait.
//!
//! Let's introduce a new event type called `CounterChangeEvent`, then indicate that `Counter` can emit this type of event:
//!
//! ```no_run
//! use gpui::EventEmitter;
//! # struct Counter {
//! # count: usize,
//! # }
//! struct CounterChangeEvent {
//! increment: usize,
//! }
//!
//! impl EventEmitter<CounterChangeEvent> for Counter {}
//! ```
//!
//! Next, the example should be updated, replacing the observation with a subscription. Whenever the counter is incremented, a `Change` event is emitted to indicate the magnitude of the increase.
//!
//! ```no_run
//! # use gpui::{App, AppContext, Application, Context, Entity, EventEmitter};
//! # struct Counter {
//! # count: usize,
//! # }
//! # struct CounterChangeEvent {
//! # increment: usize,
//! # }
//! # impl EventEmitter<CounterChangeEvent> for Counter {}
//! Application::new().run(|cx: &mut App| {
//! let first_counter: Entity<Counter> = cx.new(|_cx| Counter { count: 0 });
//!
//! let second_counter = cx.new(|cx: &mut Context<Counter>| {
//! // Note we can set up the callback before the Counter is even created!
//! cx.subscribe(&first_counter, |second: &mut Counter, _first: Entity<Counter>, event, _cx| {
//! second.count += event.increment * 2;
//! })
//! .detach();
//!
//! Counter {
//! count: first_counter.read(cx).count * 2,
//! }
//! });
//!
//! first_counter.update(cx, |first, cx| {
//! first.count += 2;
//! cx.emit(CounterChangeEvent { increment: 2 });
//! cx.notify();
//! });
//!
//! assert_eq!(second_counter.read(cx).count, 4);
//! });
//! ```
-27
View File
@@ -1,27 +0,0 @@
[package]
name = "gpui_macros"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "Macros used by gpui"
[lints]
workspace = true
[features]
inspector = []
[lib]
path = "src/gpui_macros.rs"
proc-macro = true
doctest = true
[dependencies]
heck.workspace = true
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
[dev-dependencies]
gpui = { workspace = true, features = ["inspector"] }
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-211
View File
@@ -1,211 +0,0 @@
use crate::register_action::generate_register_action;
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::quote;
use syn::{Data, DeriveInput, LitStr, Token, parse::ParseStream};
pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
let input = syn::parse_macro_input!(input as DeriveInput);
let struct_name = &input.ident;
let mut name_argument = None;
let mut deprecated_aliases = Vec::new();
let mut no_json = false;
let mut no_register = false;
let mut namespace = None;
let mut deprecated = None;
let mut doc_str: Option<String> = 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::<Token![=]>()?;
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::<Token![=]>()?;
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::<Token![=]>()?;
// Parse array of string literals
let content;
syn::bracketed!(content in meta.input);
let aliases = content.parse_terminated(
|input: ParseStream| input.parse::<LitStr>(),
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::<Token![=]>()?;
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::<Self>(_value)?)) }
};
let json_schema_fn_body = if no_json || is_unit_struct {
quote! { None }
} else {
quote! { Some(<Self as gpui::private::schemars::JsonSchema>::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::<Self>()
.map_or(false, |a| self == a)
}
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
Box::new(self.clone())
}
fn build(_value: gpui::private::serde_json::Value) -> gpui::Result<Box<dyn gpui::Action>> {
#build_fn_body
}
fn action_json_schema(
_generator: &mut gpui::private::schemars::SchemaGenerator,
) -> Option<gpui::private::schemars::Schema> {
#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
}
}
})
}
@@ -1,112 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
use crate::get_simple_attribute_field;
pub fn derive_app_context(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let Some(app_variable) = get_simple_attribute_field(&ast, "app") else {
return quote! {
compile_error!("Derive must have an #[app] attribute to detect the &mut App field");
}
.into();
};
let type_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
let r#gen = quote! {
impl #impl_generics gpui::AppContext for #type_name #type_generics
#where_clause
{
type Result<T> = T;
fn new<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T,
) -> Self::Result<gpui::Entity<T>> {
self.#app_variable.new(build_entity)
}
fn reserve_entity<T: 'static>(&mut self) -> Self::Result<gpui::Reservation<T>> {
self.#app_variable.reserve_entity()
}
fn insert_entity<T: 'static>(
&mut self,
reservation: gpui::Reservation<T>,
build_entity: impl FnOnce(&mut gpui::Context<'_, T>) -> T,
) -> Self::Result<gpui::Entity<T>> {
self.#app_variable.insert_entity(reservation, build_entity)
}
fn update_entity<T, R>(
&mut self,
handle: &gpui::Entity<T>,
update: impl FnOnce(&mut T, &mut gpui::Context<'_, T>) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.#app_variable.update_entity(handle, update)
}
fn as_mut<'y, 'z, T>(
&'y mut self,
handle: &'z gpui::Entity<T>,
) -> Self::Result<gpui::GpuiBorrow<'y, T>>
where
T: 'static,
{
self.#app_variable.as_mut(handle)
}
fn read_entity<T, R>(
&self,
handle: &gpui::Entity<T>,
read: impl FnOnce(&T, &gpui::App) -> R,
) -> Self::Result<R>
where
T: 'static,
{
self.#app_variable.read_entity(handle, read)
}
fn update_window<T, F>(&mut self, window: gpui::AnyWindowHandle, f: F) -> gpui::Result<T>
where
F: FnOnce(gpui::AnyView, &mut gpui::Window, &mut gpui::App) -> T,
{
self.#app_variable.update_window(window, f)
}
fn read_window<T, R>(
&self,
window: &gpui::WindowHandle<T>,
read: impl FnOnce(gpui::Entity<T>, &gpui::App) -> R,
) -> gpui::Result<R>
where
T: 'static,
{
self.#app_variable.read_window(window, read)
}
fn background_spawn<R>(&self, future: impl std::future::Future<Output = R> + Send + 'static) -> gpui::Task<R>
where
R: Send + 'static,
{
self.#app_variable.background_spawn(future)
}
fn read_global<G, R>(&self, callback: impl FnOnce(&G, &gpui::App) -> R) -> Self::Result<R>
where
G: gpui::Global,
{
self.#app_variable.read_global(callback)
}
}
};
r#gen.into()
}
@@ -1,305 +0,0 @@
//! Implements `#[derive_inspector_reflection]` macro to provide runtime access to trait methods
//! that have the shape `fn method(self) -> Self`. This code was generated using Zed Agent with Claude Opus 4.
use heck::ToSnakeCase as _;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::quote;
use syn::{
Attribute, Expr, FnArg, Ident, Item, ItemTrait, Lit, Meta, Path, ReturnType, TraitItem, Type,
parse_macro_input, parse_quote,
visit_mut::{self, VisitMut},
};
pub fn derive_inspector_reflection(_args: TokenStream, input: TokenStream) -> TokenStream {
let mut item = parse_macro_input!(input as Item);
// First, expand any macros in the trait
match &mut item {
Item::Trait(trait_item) => {
let mut expander = MacroExpander;
expander.visit_item_trait_mut(trait_item);
}
_ => {
return syn::Error::new_spanned(
quote!(#item),
"#[derive_inspector_reflection] can only be applied to traits",
)
.to_compile_error()
.into();
}
}
// Now process the expanded trait
match item {
Item::Trait(trait_item) => generate_reflected_trait(trait_item),
_ => unreachable!(),
}
}
fn generate_reflected_trait(trait_item: ItemTrait) -> TokenStream {
let trait_name = &trait_item.ident;
let vis = &trait_item.vis;
// Determine if we're being called from within the gpui crate
let call_site = Span::call_site();
let inspector_reflection_path = if is_called_from_gpui_crate(call_site) {
quote! { crate::inspector_reflection }
} else {
quote! { ::gpui::inspector_reflection }
};
// Collect method information for methods of form fn name(self) -> Self or fn name(mut self) -> Self
let mut method_infos = Vec::new();
for item in &trait_item.items {
if let TraitItem::Fn(method) = item {
let method_name = &method.sig.ident;
// Check if method has self or mut self receiver
let has_valid_self_receiver = method
.sig
.inputs
.iter()
.any(|arg| matches!(arg, FnArg::Receiver(r) if r.reference.is_none()));
// Check if method returns Self
let returns_self = match &method.sig.output {
ReturnType::Type(_, ty) => {
matches!(**ty, Type::Path(ref path) if path.path.is_ident("Self"))
}
ReturnType::Default => false,
};
// Check if method has exactly one parameter (self or mut self)
let param_count = method.sig.inputs.len();
// Include methods of form fn name(self) -> Self or fn name(mut self) -> Self
// This includes methods with default implementations
if has_valid_self_receiver && returns_self && param_count == 1 {
// Extract documentation and cfg attributes
let doc = extract_doc_comment(&method.attrs);
let cfg_attrs = extract_cfg_attributes(&method.attrs);
method_infos.push((method_name.clone(), doc, cfg_attrs));
}
}
}
// Generate the reflection module name
let reflection_mod_name = Ident::new(
&format!("{}_reflection", trait_name.to_string().to_snake_case()),
trait_name.span(),
);
// Generate wrapper functions for each method
// These wrappers use type erasure to allow runtime invocation
let wrapper_functions = method_infos.iter().map(|(method_name, _doc, cfg_attrs)| {
let wrapper_name = Ident::new(
&format!("__wrapper_{}", method_name),
method_name.span(),
);
quote! {
#(#cfg_attrs)*
fn #wrapper_name<T: #trait_name + 'static>(value: Box<dyn std::any::Any>) -> Box<dyn std::any::Any> {
if let Ok(concrete) = value.downcast::<T>() {
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::<T>,
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<T: #trait_name + 'static>() -> Vec<#inspector_reflection_path::FunctionReflection<T>> {
vec![
#(#method_info_entries),*
]
}
/// Find a method by name for a concrete type implementing the trait
pub fn find_method<T: #trait_name + 'static>(name: &str) -> Option<#inspector_reflection_path::FunctionReflection<T>> {
methods::<T>().into_iter().find(|m| m.name == name)
}
}
};
TokenStream::from(output)
}
fn extract_doc_comment(attrs: &[Attribute]) -> Option<String> {
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<Attribute> {
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(&macro_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<Vec<TraitItem>> {
let path = &macro_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::<Vec<_>>()
.join("::")
}
fn parse_expanded_items(expanded: TokenStream) -> Option<Vec<TraitItem>> {
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)
}
@@ -1,24 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
pub fn derive_into_element(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let type_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
let r#gen = quote! {
impl #impl_generics gpui::IntoElement for #type_name #type_generics
#where_clause
{
type Element = gpui::Component<Self>;
#[track_caller]
fn into_element(self) -> Self::Element {
gpui::Component::new(self)
}
}
};
r#gen.into()
}
-21
View File
@@ -1,21 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
pub fn derive_render(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let type_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
let r#gen = quote! {
impl #impl_generics gpui::Render for #type_name #type_generics
#where_clause
{
fn render(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context<Self>) -> impl gpui::Element {
gpui::Empty
}
}
};
r#gen.into()
}
@@ -1,71 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
use super::get_simple_attribute_field;
pub fn derive_visual_context(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let Some(window_variable) = get_simple_attribute_field(&ast, "window") else {
return quote! {
compile_error!("Derive must have a #[window] attribute to detect the &mut Window field");
}
.into();
};
let Some(app_variable) = get_simple_attribute_field(&ast, "app") else {
return quote! {
compile_error!("Derive must have a #[app] attribute to detect the &mut App field");
}
.into();
};
let type_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
let r#gen = quote! {
impl #impl_generics gpui::VisualContext for #type_name #type_generics
#where_clause
{
fn window_handle(&self) -> gpui::AnyWindowHandle {
self.#window_variable.window_handle()
}
fn update_window_entity<T: 'static, R>(
&mut self,
entity: &gpui::Entity<T>,
update: impl FnOnce(&mut T, &mut gpui::Window, &mut gpui::Context<T>) -> R,
) -> Self::Result<R> {
gpui::AppContext::update_entity(self.#app_variable, entity, |entity, cx| update(entity, self.#window_variable, cx))
}
fn new_window_entity<T: 'static>(
&mut self,
build_entity: impl FnOnce(&mut gpui::Window, &mut gpui::Context<'_, T>) -> T,
) -> Self::Result<gpui::Entity<T>> {
gpui::AppContext::new(self.#app_variable, |cx| build_entity(self.#window_variable, cx))
}
fn replace_root_view<V>(
&mut self,
build_view: impl FnOnce(&mut gpui::Window, &mut gpui::Context<V>) -> V,
) -> Self::Result<gpui::Entity<V>>
where
V: 'static + gpui::Render,
{
self.#window_variable.replace_root(self.#app_variable, build_view)
}
fn focus<V>(&mut self, entity: &gpui::Entity<V>) -> Self::Result<()>
where
V: gpui::Focusable,
{
let focus_handle = gpui::Focusable::focus_handle(entity, self.#app_variable);
self.#window_variable.focus(&focus_handle)
}
}
};
r#gen.into()
}
-223
View File
@@ -1,223 +0,0 @@
mod derive_action;
mod derive_app_context;
mod derive_into_element;
mod derive_render;
mod derive_visual_context;
mod register_action;
mod styles;
mod test;
#[cfg(any(feature = "inspector", debug_assertions))]
mod derive_inspector_reflection;
use proc_macro::TokenStream;
use syn::{DeriveInput, Ident};
/// `Action` derive macro - see the trait documentation for details.
#[proc_macro_derive(Action, attributes(action))]
pub fn derive_action(input: TokenStream) -> TokenStream {
derive_action::derive_action(input)
}
/// This can be used to register an action with the GPUI runtime when you want to manually implement
/// the `Action` trait. Typically you should use the `Action` derive macro or `actions!` macro
/// instead.
#[proc_macro]
pub fn register_action(ident: TokenStream) -> TokenStream {
register_action::register_action(ident)
}
/// #[derive(IntoElement)] is used to create a Component out of anything that implements
/// the `RenderOnce` trait.
#[proc_macro_derive(IntoElement)]
pub fn derive_into_element(input: TokenStream) -> TokenStream {
derive_into_element::derive_into_element(input)
}
#[proc_macro_derive(Render)]
#[doc(hidden)]
pub fn derive_render(input: TokenStream) -> TokenStream {
derive_render::derive_render(input)
}
/// #[derive(AppContext)] is used to create a context out of anything that holds a `&mut App`
/// Note that a `#[app]` attribute is required to identify the variable holding the &mut App.
///
/// Failure to add the attribute causes a compile error:
///
/// ```compile_fail
/// # #[macro_use] extern crate gpui_macros;
/// # #[macro_use] extern crate gpui;
/// #[derive(AppContext)]
/// struct MyContext<'a> {
/// app: &'a mut gpui::App
/// }
/// ```
#[proc_macro_derive(AppContext, attributes(app))]
pub fn derive_app_context(input: TokenStream) -> TokenStream {
derive_app_context::derive_app_context(input)
}
/// #[derive(VisualContext)] is used to create a visual context out of anything that holds a `&mut Window` and
/// implements `AppContext`
/// Note that a `#[app]` and a `#[window]` attribute are required to identify the variables holding the &mut App,
/// and &mut Window respectively.
///
/// Failure to add both attributes causes a compile error:
///
/// ```compile_fail
/// # #[macro_use] extern crate gpui_macros;
/// # #[macro_use] extern crate gpui;
/// #[derive(VisualContext)]
/// struct MyContext<'a, 'b> {
/// #[app]
/// app: &'a mut gpui::App,
/// window: &'b mut gpui::Window
/// }
/// ```
///
/// ```compile_fail
/// # #[macro_use] extern crate gpui_macros;
/// # #[macro_use] extern crate gpui;
/// #[derive(VisualContext)]
/// struct MyContext<'a, 'b> {
/// app: &'a mut gpui::App,
/// #[window]
/// window: &'b mut gpui::Window
/// }
/// ```
#[proc_macro_derive(VisualContext, attributes(window, app))]
pub fn derive_visual_context(input: TokenStream) -> TokenStream {
derive_visual_context::derive_visual_context(input)
}
/// Used by GPUI to generate the style helpers.
#[proc_macro]
#[doc(hidden)]
pub fn style_helpers(input: TokenStream) -> TokenStream {
styles::style_helpers(input)
}
/// Generates methods for visibility styles.
#[proc_macro]
pub fn visibility_style_methods(input: TokenStream) -> TokenStream {
styles::visibility_style_methods(input)
}
/// Generates methods for margin styles.
#[proc_macro]
pub fn margin_style_methods(input: TokenStream) -> TokenStream {
styles::margin_style_methods(input)
}
/// Generates methods for padding styles.
#[proc_macro]
pub fn padding_style_methods(input: TokenStream) -> TokenStream {
styles::padding_style_methods(input)
}
/// Generates methods for position styles.
#[proc_macro]
pub fn position_style_methods(input: TokenStream) -> TokenStream {
styles::position_style_methods(input)
}
/// Generates methods for overflow styles.
#[proc_macro]
pub fn overflow_style_methods(input: TokenStream) -> TokenStream {
styles::overflow_style_methods(input)
}
/// Generates methods for cursor styles.
#[proc_macro]
pub fn cursor_style_methods(input: TokenStream) -> TokenStream {
styles::cursor_style_methods(input)
}
/// Generates methods for border styles.
#[proc_macro]
pub fn border_style_methods(input: TokenStream) -> TokenStream {
styles::border_style_methods(input)
}
/// Generates methods for box shadow styles.
#[proc_macro]
pub fn box_shadow_style_methods(input: TokenStream) -> TokenStream {
styles::box_shadow_style_methods(input)
}
/// `#[gpui::test]` can be used to annotate test functions that run with GPUI support.
///
/// It supports both synchronous and asynchronous tests, and can provide you with
/// as many `TestAppContext` instances as you need.
/// The output contains a `#[test]` annotation so this can be used with any existing
/// test harness (`cargo test` or `cargo-nextest`).
///
/// ```
/// #[gpui::test]
/// async fn test_foo(mut cx: &TestAppContext) { }
/// ```
///
/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance.
/// this will be seeded with the `SEED` environment variable and is used internally by
/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests.
/// Using the same `StdRng` for behavior in your test will allow you to exercise a wide
/// variety of scenarios and interleavings just by changing the seed.
///
/// # Arguments
///
/// - `#[gpui::test]` with no arguments runs once with the seed `0` or `SEED` env var if set.
/// - `#[gpui::test(seed = 10)]` runs once with the seed `10`.
/// - `#[gpui::test(seeds(10, 20, 30))]` runs three times with seeds `10`, `20`, and `30`.
/// - `#[gpui::test(iterations = 5)]` runs five times, providing as seed the values in the range `0..5`.
/// - `#[gpui::test(retries = 3)]` runs up to four times if it fails to try and make it pass.
/// - `#[gpui::test(on_failure = "crate::test::report_failure")]` will call the specified function after the
/// tests fail so that you can write out more detail about the failure.
///
/// You can combine `iterations = ...` with `seeds(...)`:
/// - `#[gpui::test(iterations = 5, seed = 10)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10))]`.
/// - `#[gpui::test(iterations = 5, seeds(10, 20, 30)]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10, 20, 30))]`.
/// - `#[gpui::test(seeds(10, 20, 30), iterations = 5]` is equivalent to `#[gpui::test(seeds(0, 1, 2, 3, 4, 10, 20, 30))]`.
///
/// # Environment Variables
///
/// - `SEED`: sets a seed for the first run
/// - `ITERATIONS`: forces the value of the `iterations` argument
#[proc_macro_attribute]
pub fn test(args: TokenStream, function: TokenStream) -> TokenStream {
test::test(args, function)
}
/// When added to a trait, `#[derive_inspector_reflection]` generates a module which provides
/// enumeration and lookup by name of all methods that have the shape `fn method(self) -> Self`.
/// This is used by the inspector so that it can use the builder methods in `Styled` and
/// `StyledExt`.
///
/// The generated module will have the name `<snake_case_trait_name>_reflection` and contain the
/// following functions:
///
/// ```ignore
/// pub fn methods::<T: TheTrait + 'static>() -> Vec<gpui::inspector_reflection::FunctionReflection<T>>;
///
/// pub fn find_method::<T: TheTrait + 'static>() -> Option<gpui::inspector_reflection::FunctionReflection<T>>;
/// ```
///
/// 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<Ident> {
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,
}
}
-47
View File
@@ -1,47 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use syn::parse_macro_input;
pub(crate) fn register_action(ident: TokenStream) -> TokenStream {
let name = parse_macro_input!(ident as Ident);
let registration = generate_register_action(&name);
TokenStream::from(quote! {
#registration
})
}
pub(crate) fn generate_register_action(type_name: &Ident) -> TokenStream2 {
let action_builder_fn_name = format_ident!(
"__gpui_actions_builder_{}",
type_name.to_string().to_lowercase()
);
quote! {
impl #type_name {
/// This is an auto generated function, do not use.
#[automatically_derived]
#[doc(hidden)]
fn __autogenerated() {
/// This is an auto generated function, do not use.
#[doc(hidden)]
fn #action_builder_fn_name() -> gpui::MacroActionData {
gpui::MacroActionData {
name: <#type_name as gpui::Action>::name_for_type(),
type_id: ::std::any::TypeId::of::<#type_name>(),
build: <#type_name as gpui::Action>::build,
json_schema: <#type_name as gpui::Action>::action_json_schema,
deprecated_aliases: <#type_name as gpui::Action>::deprecated_aliases(),
deprecation_message: <#type_name as gpui::Action>::deprecation_message(),
documentation: <#type_name as gpui::Action>::documentation(),
}
}
gpui::private::inventory::submit! {
gpui::MacroActionBuilder(#action_builder_fn_name)
}
}
}
}
}
File diff suppressed because it is too large Load Diff
-330
View File
@@ -1,330 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::Ident;
use quote::{format_ident, quote};
use std::mem;
use syn::{
self, Expr, ExprLit, FnArg, ItemFn, Lit, Meta, MetaList, PathSegment, Token, Type,
parse::{Parse, ParseStream},
parse_quote,
punctuated::Punctuated,
spanned::Spanned,
};
struct Args {
seeds: Vec<u64>,
max_retries: usize,
max_iterations: usize,
on_failure_fn_name: proc_macro2::TokenStream,
}
impl Parse for Args {
fn parse(input: ParseStream) -> Result<Self, syn::Error> {
let mut seeds = Vec::<u64>::new();
let mut max_retries = 0;
let mut max_iterations = 1;
let mut on_failure_fn_name = quote!(None);
let metas = Punctuated::<Meta, Token![,]>::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::<ItemFn>(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<syn::Attribute>,
inner_fn_name: Ident,
outer_fn_name: Ident,
) -> Result<TokenStream, TokenStream> {
let seeds = &args.seeds;
let max_retries = args.max_retries;
let num_iterations = args.max_iterations;
let on_failure_fn_name = &args.on_failure_fn_name;
let seeds = quote!( #(#seeds),* );
let mut outer_fn: ItemFn = if inner_fn.sig.asyncness.is_some() {
// Pass to the test function the number of app contexts that it needs,
// based on its parameter list.
let mut cx_vars = proc_macro2::TokenStream::new();
let mut cx_teardowns = proc_macro2::TokenStream::new();
let mut inner_fn_args = proc_macro2::TokenStream::new();
for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() {
if let FnArg::Typed(arg) = arg {
if let Type::Path(ty) = &*arg.ty {
let last_segment = ty.path.segments.last();
match last_segment.map(|s| s.ident.to_string()).as_deref() {
Some("StdRng") => {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
continue;
}
Some("BackgroundExecutor") => {
inner_fn_args.extend(quote!(gpui::BackgroundExecutor::new(
std::sync::Arc::new(dispatcher.clone()),
),));
continue;
}
_ => {}
}
} else if let Type::Reference(ty) = &*arg.ty
&& let Type::Path(ty) = &*ty.elem
{
let last_segment = ty.path.segments.last();
if let Some("TestAppContext") =
last_segment.map(|s| s.ident.to_string()).as_deref()
{
let cx_varname = format_ident!("cx_{}", ix);
cx_vars.extend(quote!(
let mut #cx_varname = gpui::TestAppContext::build(
dispatcher.clone(),
Some(stringify!(#outer_fn_name)),
);
));
cx_teardowns.extend(quote!(
dispatcher.run_until_parked();
#cx_varname.executor().forbid_parking();
#cx_varname.quit();
dispatcher.run_until_parked();
));
inner_fn_args.extend(quote!(&mut #cx_varname,));
continue;
}
}
}
return Err(error_with_message("invalid function signature", arg));
}
parse_quote! {
#[test]
fn #outer_fn_name() {
#inner_fn
gpui::run_test(
#num_iterations,
&[#seeds],
#max_retries,
&mut |dispatcher, _seed| {
let executor = gpui::BackgroundExecutor::new(std::sync::Arc::new(dispatcher.clone()));
#cx_vars
executor.block_test(#inner_fn_name(#inner_fn_args));
#cx_teardowns
},
#on_failure_fn_name
);
}
}
} else {
// Pass to the test function the number of app contexts that it needs,
// based on its parameter list.
let mut cx_vars = proc_macro2::TokenStream::new();
let mut cx_teardowns = proc_macro2::TokenStream::new();
let mut inner_fn_args = proc_macro2::TokenStream::new();
for (ix, arg) in inner_fn.sig.inputs.iter().enumerate() {
if let FnArg::Typed(arg) = arg {
if let Type::Path(ty) = &*arg.ty {
let last_segment = ty.path.segments.last();
if let Some("StdRng") = last_segment.map(|s| s.ident.to_string()).as_deref() {
inner_fn_args.extend(quote!(rand::SeedableRng::seed_from_u64(_seed),));
continue;
}
} else if let Type::Reference(ty) = &*arg.ty
&& let Type::Path(ty) = &*ty.elem
{
let last_segment = ty.path.segments.last();
match last_segment.map(|s| s.ident.to_string()).as_deref() {
Some("App") => {
let cx_varname = format_ident!("cx_{}", ix);
let cx_varname_lock = format_ident!("cx_{}_lock", ix);
cx_vars.extend(quote!(
let mut #cx_varname = gpui::TestAppContext::build(
dispatcher.clone(),
Some(stringify!(#outer_fn_name))
);
let mut #cx_varname_lock = #cx_varname.app.borrow_mut();
));
inner_fn_args.extend(quote!(&mut #cx_varname_lock,));
cx_teardowns.extend(quote!(
drop(#cx_varname_lock);
dispatcher.run_until_parked();
#cx_varname.update(|cx| { cx.background_executor().forbid_parking(); cx.quit(); });
dispatcher.run_until_parked();
));
continue;
}
Some("TestAppContext") => {
let cx_varname = format_ident!("cx_{}", ix);
cx_vars.extend(quote!(
let mut #cx_varname = gpui::TestAppContext::build(
dispatcher.clone(),
Some(stringify!(#outer_fn_name))
);
));
cx_teardowns.extend(quote!(
dispatcher.run_until_parked();
#cx_varname.executor().forbid_parking();
#cx_varname.quit();
dispatcher.run_until_parked();
));
inner_fn_args.extend(quote!(&mut #cx_varname,));
continue;
}
_ => {}
}
}
}
return Err(error_with_message("invalid function signature", arg));
}
parse_quote! {
#[test]
fn #outer_fn_name() {
#inner_fn
gpui::run_test(
#num_iterations,
&[#seeds],
#max_retries,
&mut |dispatcher, _seed| {
#cx_vars
#inner_fn_name(#inner_fn_args);
#cx_teardowns
},
#on_failure_fn_name,
);
}
}
};
outer_fn.attrs.extend(inner_fn_attributes);
Ok(TokenStream::from(quote!(#outer_fn)))
}
fn parse_usize_from_expr(expr: &Expr) -> Result<usize, syn::Error> {
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<Vec<u64>, syn::Error> {
let mut result = Vec::new();
let tokens = &meta_list.tokens;
let parser = |input: ParseStream| {
let exprs = Punctuated::<Expr, Token![,]>::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())
}
@@ -1,13 +0,0 @@
#[test]
fn test_derive_context() {
use gpui::{App, Window};
use gpui_macros::{AppContext, VisualContext};
#[derive(AppContext, VisualContext)]
struct _MyCustomContext<'a, 'b> {
#[app]
app: &'a mut App,
#[window]
window: &'b mut Window,
}
}
@@ -1,133 +0,0 @@
//! This code was generated using Zed Agent with Claude Opus 4.
// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv
#[cfg_attr(not(rust_analyzer), gpui_macros::derive_inspector_reflection)]
trait Transform: Clone {
/// Doubles the value
fn double(self) -> Self;
/// Triples the value
fn triple(self) -> Self;
/// Increments the value by one
///
/// This method has a default implementation
fn increment(self) -> Self {
// Default implementation
self.add_one()
}
/// Quadruples the value by doubling twice
fn quadruple(self) -> Self {
// Default implementation with mut self
self.double().double()
}
// These methods will be filtered out:
#[allow(dead_code)]
fn add(&self, other: &Self) -> Self;
#[allow(dead_code)]
fn set_value(&mut self, value: i32);
#[allow(dead_code)]
fn get_value(&self) -> i32;
/// Adds one to the value
fn add_one(self) -> Self;
}
#[derive(Debug, Clone, PartialEq)]
struct Number(i32);
impl Transform for Number {
fn double(self) -> Self {
Number(self.0 * 2)
}
fn triple(self) -> Self {
Number(self.0 * 3)
}
fn add(&self, other: &Self) -> Self {
Number(self.0 + other.0)
}
fn set_value(&mut self, value: i32) {
self.0 = value;
}
fn get_value(&self) -> i32 {
self.0
}
fn add_one(self) -> Self {
Number(self.0 + 1)
}
}
#[test]
fn test_derive_inspector_reflection() {
use transform_reflection::*;
// Get all methods that match the pattern fn(self) -> Self or fn(mut self) -> Self
let methods = methods::<Number>();
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::<Number>("double").unwrap().invoke(num.clone());
assert_eq!(doubled, Number(10));
let tripled = find_method::<Number>("triple").unwrap().invoke(num.clone());
assert_eq!(tripled, Number(15));
let incremented = find_method::<Number>("increment")
.unwrap()
.invoke(num.clone());
assert_eq!(incremented, Number(6));
let quadrupled = find_method::<Number>("quadruple").unwrap().invoke(num);
assert_eq!(quadrupled, Number(20));
// Try to invoke a non-existent method
let result = find_method::<Number>("nonexistent");
assert!(result.is_none());
// Chain operations
let num = Number(10);
let result = find_method::<Number>("double")
.map(|m| m.invoke(num))
.and_then(|n| find_method::<Number>("increment").map(|m| m.invoke(n)))
.and_then(|n| find_method::<Number>("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::<Number>("double").unwrap();
assert_eq!(double_method.documentation, Some("Doubles the value"));
let triple_method = find_method::<Number>("triple").unwrap();
assert_eq!(triple_method.documentation, Some("Triples the value"));
let increment_method = find_method::<Number>("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::<Number>("quadruple").unwrap();
assert_eq!(
quadruple_method.documentation,
Some("Quadruples the value by doubling twice")
);
let add_one_method = find_method::<Number>("add_one").unwrap();
assert_eq!(add_one_method.documentation, Some("Adds one to the value"));
}
-7
View File
@@ -1,7 +0,0 @@
#[test]
fn test_derive_render() {
use gpui_macros::Render;
#[derive(Render)]
struct _Element;
}
-19
View File
@@ -1,19 +0,0 @@
[package]
name = "gpui_tokio"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/gpui_tokio.rs"
doctest = false
[dependencies]
anyhow.workspace = true
util.workspace = true
gpui.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-81
View File
@@ -1,81 +0,0 @@
use std::future::Future;
use gpui::{App, AppContext, Global, ReadGlobal, Task};
use util::defer;
pub use tokio::task::JoinError;
pub fn init(cx: &mut App) {
cx.set_global(GlobalTokio::new());
}
struct GlobalTokio {
runtime: tokio::runtime::Runtime,
}
impl Global for GlobalTokio {}
impl GlobalTokio {
fn new() -> Self {
let runtime = tokio::runtime::Builder::new_multi_thread()
// Since we now have two executors, let's try to keep our footprint small
.worker_threads(2)
.enable_all()
.build()
.expect("Failed to initialize Tokio");
Self { runtime }
}
}
pub struct Tokio {}
impl Tokio {
/// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task
/// Note that the Tokio task will be cancelled if the GPUI task is dropped
pub fn spawn<C, Fut, R>(cx: &C, f: Fut) -> C::Result<Task<Result<R, JoinError>>>
where
C: AppContext,
Fut: Future<Output = R> + Send + 'static,
R: Send + 'static,
{
cx.read_global(|tokio: &GlobalTokio, cx| {
let join_handle = tokio.runtime.spawn(f);
let abort_handle = join_handle.abort_handle();
let cancel = defer(move || {
abort_handle.abort();
});
cx.background_spawn(async move {
let result = join_handle.await;
drop(cancel);
result
})
})
}
/// Spawns the given future on Tokio's thread pool, and returns it via a GPUI task
/// Note that the Tokio task will be cancelled if the GPUI task is dropped
pub fn spawn_result<C, Fut, R>(cx: &C, f: Fut) -> C::Result<Task<anyhow::Result<R>>>
where
C: AppContext,
Fut: Future<Output = anyhow::Result<R>> + Send + 'static,
R: Send + 'static,
{
cx.read_global(|tokio: &GlobalTokio, cx| {
let join_handle = tokio.runtime.spawn(f);
let abort_handle = join_handle.abort_handle();
let cancel = defer(move || {
abort_handle.abort();
});
cx.background_spawn(async move {
let result = join_handle.await?;
drop(cancel);
result
})
})
}
pub fn handle(cx: &App) -> tokio::runtime::Handle {
GlobalTokio::global(cx).runtime.handle().clone()
}
}
-26
View File
@@ -1,26 +0,0 @@
[package]
name = "html_to_markdown"
version = "0.1.0"
description = "Convert HTML to Markdown"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/html_to_markdown"
keywords = ["html", "markdown", "html-to-markdown"]
edition.workspace = true
publish = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/html_to_markdown.rs"
[dependencies]
anyhow.workspace = true
html5ever.workspace = true
markup5ever_rcdom.workspace = true
regex.workspace = true
[dev-dependencies]
indoc.workspace = true
pretty_assertions.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
@@ -1,83 +0,0 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::sync::OnceLock;
use html5ever::Attribute;
/// Returns a [`HashSet`] containing the HTML elements that are inline by default.
///
/// [MDN: List of "inline" elements](https://yari-demos.prod.mdn.mozit.cloud/en-US/docs/Web/HTML/Inline_elements)
fn inline_elements() -> &'static HashSet<&'static str> {
static INLINE_ELEMENTS: OnceLock<HashSet<&str>> = OnceLock::new();
INLINE_ELEMENTS.get_or_init(|| {
HashSet::from_iter([
"a", "abbr", "acronym", "audio", "b", "bdi", "bdo", "big", "br", "button", "canvas",
"cite", "code", "data", "datalist", "del", "dfn", "em", "embed", "i", "iframe", "img",
"input", "ins", "kbd", "label", "map", "mark", "meter", "noscript", "object", "output",
"picture", "progress", "q", "ruby", "s", "samp", "script", "select", "slot", "small",
"span", "strong", "sub", "sup", "svg", "template", "textarea", "time", "tt", "u",
"var", "video", "wbr",
])
})
}
#[derive(Debug, Clone)]
pub struct HtmlElement {
tag: String,
pub(crate) attrs: RefCell<Vec<Attribute>>,
}
impl HtmlElement {
pub fn new(tag: String, attrs: RefCell<Vec<Attribute>>) -> Self {
Self { tag, attrs }
}
pub fn tag(&self) -> &str {
&self.tag
}
/// Returns whether this [`HtmlElement`] is an inline element.
pub fn is_inline(&self) -> bool {
inline_elements().contains(self.tag.as_str())
}
/// Returns the attribute with the specified name.
pub fn attr(&self, name: &str) -> Option<String> {
self.attrs
.borrow()
.iter()
.find(|attr| attr.name.local.to_string() == name)
.map(|attr| attr.value.to_string())
}
/// Returns the list of classes on this [`HtmlElement`].
pub fn classes(&self) -> Vec<String> {
self.attrs
.borrow()
.iter()
.find(|attr| attr.name.local.to_string() == "class")
.map(|attr| {
attr.value
.split(' ')
.map(|class| class.trim().to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
/// Returns whether this [`HtmlElement`] has the specified class.
pub fn has_class(&self, class: &str) -> bool {
self.has_any_classes(&[class])
}
/// Returns whether this [`HtmlElement`] has any of the specified classes.
pub fn has_any_classes(&self, classes: &[&str]) -> bool {
self.attrs.borrow().iter().any(|attr| {
attr.name.local.to_string() == "class"
&& attr
.value
.split(' ')
.any(|class| classes.contains(&class.trim()))
})
}
}
@@ -1,46 +0,0 @@
//! Convert HTML to Markdown.
mod html_element;
pub mod markdown;
mod markdown_writer;
pub mod structure;
use std::io::Read;
use anyhow::{Context as _, Result};
use html5ever::driver::ParseOpts;
use html5ever::parse_document;
use html5ever::tendril::TendrilSink;
use html5ever::tree_builder::TreeBuilderOpts;
use markup5ever_rcdom::RcDom;
pub use crate::html_element::*;
pub use crate::markdown_writer::*;
/// Converts the provided HTML to Markdown.
pub fn convert_html_to_markdown(html: impl Read, handlers: &mut [TagHandler]) -> Result<String> {
let dom = parse_html(html).context("failed to parse HTML")?;
let markdown_writer = MarkdownWriter::new();
let markdown = markdown_writer
.run(&dom.document, handlers)
.context("failed to convert HTML to Markdown")?;
Ok(markdown)
}
fn parse_html(mut html: impl Read) -> Result<RcDom> {
let parse_options = ParseOpts {
tree_builder: TreeBuilderOpts {
drop_doctype: true,
..Default::default()
},
..Default::default()
};
let dom = parse_document(RcDom::default(), parse_options)
.from_utf8()
.read_from(&mut html)
.context("failed to parse HTML document")?;
Ok(dom)
}
-276
View File
@@ -1,276 +0,0 @@
use crate::html_element::HtmlElement;
use crate::markdown_writer::{HandleTag, HandlerOutcome, MarkdownWriter, StartTagOutcome};
pub struct WebpageChromeRemover;
impl HandleTag for WebpageChromeRemover {
fn should_handle(&self, tag: &str) -> bool {
matches!(tag, "head" | "script" | "style" | "nav")
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
_writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"head" | "script" | "style" | "nav" => return StartTagOutcome::Skip,
_ => {}
}
StartTagOutcome::Continue
}
}
pub struct ParagraphHandler;
impl HandleTag for ParagraphHandler {
fn should_handle(&self, _tag: &str) -> bool {
true
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
if tag.is_inline()
&& writer.is_inside("p")
&& let Some(parent) = writer.current_element_stack().iter().last()
&& !(parent.is_inline()
|| writer.markdown.ends_with(' ')
|| writer.markdown.ends_with('\n'))
{
writer.push_str(" ");
}
if tag.tag() == "p" {
writer.push_blank_line()
}
StartTagOutcome::Continue
}
}
pub struct HeadingHandler;
impl HandleTag for HeadingHandler {
fn should_handle(&self, tag: &str) -> bool {
matches!(tag, "h1" | "h2" | "h3" | "h4" | "h5" | "h6")
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"h1" => writer.push_str("\n\n# "),
"h2" => writer.push_str("\n\n## "),
"h3" => writer.push_str("\n\n### "),
"h4" => writer.push_str("\n\n#### "),
"h5" => writer.push_str("\n\n##### "),
"h6" => writer.push_str("\n\n###### "),
_ => {}
}
StartTagOutcome::Continue
}
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
match tag.tag() {
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => writer.push_blank_line(),
_ => {}
}
}
}
pub struct ListHandler;
impl HandleTag for ListHandler {
fn should_handle(&self, tag: &str) -> bool {
matches!(tag, "ul" | "ol" | "li")
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"ul" | "ol" => writer.push_newline(),
"li" => writer.push_str("- "),
_ => {}
}
StartTagOutcome::Continue
}
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
match tag.tag() {
"ul" | "ol" => writer.push_newline(),
"li" => writer.push_newline(),
_ => {}
}
}
}
pub struct TableHandler {
/// The number of columns in the current `<table>`.
current_table_columns: usize,
is_first_th: bool,
is_first_td: bool,
}
impl TableHandler {
pub fn new() -> Self {
Self {
current_table_columns: 0,
is_first_th: true,
is_first_td: true,
}
}
}
impl Default for TableHandler {
fn default() -> Self {
Self::new()
}
}
impl HandleTag for TableHandler {
fn should_handle(&self, tag: &str) -> bool {
matches!(tag, "table" | "thead" | "tbody" | "tr" | "th" | "td")
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"thead" => writer.push_blank_line(),
"tr" => writer.push_newline(),
"th" => {
self.current_table_columns += 1;
if self.is_first_th {
self.is_first_th = false;
} else {
writer.push_str(" ");
}
writer.push_str("| ");
}
"td" => {
if self.is_first_td {
self.is_first_td = false;
} else {
writer.push_str(" ");
}
writer.push_str("| ");
}
_ => {}
}
StartTagOutcome::Continue
}
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
match tag.tag() {
"thead" => {
writer.push_newline();
for ix in 0..self.current_table_columns {
if ix > 0 {
writer.push_str(" ");
}
writer.push_str("| ---");
}
writer.push_str(" |");
self.is_first_th = true;
}
"tr" => {
writer.push_str(" |");
self.is_first_td = true;
}
"table" => {
self.current_table_columns = 0;
}
_ => {}
}
}
}
pub struct StyledTextHandler;
impl HandleTag for StyledTextHandler {
fn should_handle(&self, tag: &str) -> bool {
matches!(tag, "strong" | "em")
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"strong" => writer.push_str("**"),
"em" => writer.push_str("_"),
_ => {}
}
StartTagOutcome::Continue
}
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
match tag.tag() {
"strong" => writer.push_str("**"),
"em" => writer.push_str("_"),
_ => {}
}
}
}
pub struct CodeHandler;
impl HandleTag for CodeHandler {
fn should_handle(&self, tag: &str) -> bool {
matches!(tag, "pre" | "code")
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"code" => {
if !writer.is_inside("pre") {
writer.push_str("`");
}
}
"pre" => writer.push_str("\n\n```\n"),
_ => {}
}
StartTagOutcome::Continue
}
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
match tag.tag() {
"code" => {
if !writer.is_inside("pre") {
writer.push_str("`");
}
}
"pre" => writer.push_str("\n```\n"),
_ => {}
}
}
fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
if writer.is_inside("pre") {
writer.push_str(text);
return HandlerOutcome::Handled;
}
HandlerOutcome::NoOp
}
}
@@ -1,199 +0,0 @@
use std::collections::VecDeque;
use std::rc::Rc;
use std::{cell::RefCell, sync::LazyLock};
use anyhow::Result;
use markup5ever_rcdom::{Handle, NodeData};
use regex::Regex;
use crate::html_element::HtmlElement;
fn empty_line_regex() -> &'static Regex {
static REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^\s*$").expect("Failed to create empty_line_regex"));
&REGEX
}
fn more_than_three_newlines_regex() -> &'static Regex {
static REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap());
&REGEX
}
pub enum StartTagOutcome {
Continue,
Skip,
}
pub type TagHandler = Rc<RefCell<dyn HandleTag>>;
pub struct MarkdownWriter {
current_element_stack: VecDeque<HtmlElement>,
pub(crate) markdown: String,
}
impl Default for MarkdownWriter {
fn default() -> Self {
Self::new()
}
}
impl MarkdownWriter {
pub fn new() -> Self {
Self {
current_element_stack: VecDeque::new(),
markdown: String::new(),
}
}
pub fn current_element_stack(&self) -> &VecDeque<HtmlElement> {
&self.current_element_stack
}
pub fn is_inside(&self, tag: &str) -> bool {
self.current_element_stack
.iter()
.any(|parent_element| parent_element.tag() == tag)
}
/// Appends the given string slice onto the end of the Markdown output.
pub fn push_str(&mut self, str: &str) {
self.markdown.push_str(str);
}
/// Appends a newline to the end of the Markdown output.
pub fn push_newline(&mut self) {
self.push_str("\n");
}
/// Appends a blank line to the end of the Markdown output.
pub fn push_blank_line(&mut self) {
self.push_str("\n\n");
}
pub fn run(mut self, root_node: &Handle, handlers: &mut [TagHandler]) -> Result<String> {
self.visit_node(root_node, handlers)?;
Ok(Self::prettify_markdown(self.markdown))
}
fn prettify_markdown(markdown: String) -> String {
let markdown = empty_line_regex().replace_all(&markdown, "");
let markdown = more_than_three_newlines_regex().replace_all(&markdown, "\n\n");
markdown.trim().to_string()
}
fn visit_node(&mut self, node: &Handle, handlers: &mut [TagHandler]) -> Result<()> {
let mut current_element = None;
match node.data {
NodeData::Document
| NodeData::Doctype { .. }
| NodeData::ProcessingInstruction { .. }
| NodeData::Comment { .. } => {
// Currently left unimplemented, as we're not interested in this data
// at this time.
}
NodeData::Element {
ref name,
ref attrs,
..
} => {
let tag_name = name.local.to_string();
if !tag_name.is_empty() {
current_element = Some(HtmlElement::new(tag_name, attrs.clone()));
}
}
NodeData::Text { ref contents } => {
let text = contents.borrow().to_string();
self.visit_text(text, handlers)?;
}
}
if let Some(current_element) = current_element.as_ref() {
match self.start_tag(current_element, handlers) {
StartTagOutcome::Continue => {}
StartTagOutcome::Skip => return Ok(()),
}
self.current_element_stack
.push_back(current_element.clone());
}
if self.current_element_stack.len() < 200 {
for child in node.children.borrow().iter() {
self.visit_node(child, handlers)?;
}
}
if let Some(current_element) = current_element {
self.current_element_stack.pop_back();
self.end_tag(&current_element, handlers);
}
Ok(())
}
fn start_tag(&mut self, tag: &HtmlElement, handlers: &mut [TagHandler]) -> StartTagOutcome {
for handler in handlers {
if handler.borrow().should_handle(tag.tag()) {
match handler.borrow_mut().handle_tag_start(tag, self) {
StartTagOutcome::Continue => {}
StartTagOutcome::Skip => return StartTagOutcome::Skip,
}
}
}
StartTagOutcome::Continue
}
fn end_tag(&mut self, tag: &HtmlElement, handlers: &mut [TagHandler]) {
for handler in handlers {
if handler.borrow().should_handle(tag.tag()) {
handler.borrow_mut().handle_tag_end(tag, self);
}
}
}
fn visit_text(&mut self, text: String, handlers: &mut [TagHandler]) -> Result<()> {
for handler in handlers {
match handler.borrow_mut().handle_text(&text, self) {
HandlerOutcome::Handled => return Ok(()),
HandlerOutcome::NoOp => {}
}
}
let text = text
.trim_matches(|char| char == '\n' || char == '\r' || char == '\t')
.replace('\n', " ");
self.push_str(&text);
Ok(())
}
}
pub enum HandlerOutcome {
Handled,
NoOp,
}
pub trait HandleTag {
/// Returns whether this handler should handle the given tag.
fn should_handle(&self, tag: &str) -> bool;
/// Handles the start of the given tag.
fn handle_tag_start(
&mut self,
_tag: &HtmlElement,
_writer: &mut MarkdownWriter,
) -> StartTagOutcome {
StartTagOutcome::Continue
}
/// Handles the end of the given tag.
fn handle_tag_end(&mut self, _tag: &HtmlElement, _writer: &mut MarkdownWriter) {}
fn handle_text(&mut self, _text: &str, _writer: &mut MarkdownWriter) -> HandlerOutcome {
HandlerOutcome::NoOp
}
}
-1
View File
@@ -1 +0,0 @@
pub mod wikipedia;
@@ -1,181 +0,0 @@
use crate::HandleTag;
use crate::html_element::HtmlElement;
use crate::markdown_writer::{HandlerOutcome, MarkdownWriter, StartTagOutcome};
pub struct WikipediaChromeRemover;
impl HandleTag for WikipediaChromeRemover {
fn should_handle(&self, _tag: &str) -> bool {
true
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
_writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"head" | "script" | "style" | "nav" => return StartTagOutcome::Skip,
"sup" => {
if tag.has_class("reference") {
return StartTagOutcome::Skip;
}
}
"div" | "span" | "a" => {
if tag.attr("id").as_deref() == Some("p-lang-btn") {
return StartTagOutcome::Skip;
}
if tag.attr("id").as_deref() == Some("p-search") {
return StartTagOutcome::Skip;
}
let classes_to_skip = ["noprint", "mw-editsection", "mw-jump-link"];
if tag.has_any_classes(&classes_to_skip) {
return StartTagOutcome::Skip;
}
}
_ => {}
}
StartTagOutcome::Continue
}
}
pub struct WikipediaInfoboxHandler;
impl HandleTag for WikipediaInfoboxHandler {
fn should_handle(&self, tag: &str) -> bool {
tag == "table"
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
_writer: &mut MarkdownWriter,
) -> StartTagOutcome {
if tag.tag() == "table" && tag.has_class("infobox") {
return StartTagOutcome::Skip;
}
StartTagOutcome::Continue
}
}
pub struct WikipediaCodeHandler {
language: Option<String>,
}
impl WikipediaCodeHandler {
pub fn new() -> Self {
Self { language: None }
}
}
impl Default for WikipediaCodeHandler {
fn default() -> Self {
Self::new()
}
}
impl HandleTag for WikipediaCodeHandler {
fn should_handle(&self, tag: &str) -> bool {
matches!(tag, "div" | "pre" | "code")
}
fn handle_tag_start(
&mut self,
tag: &HtmlElement,
writer: &mut MarkdownWriter,
) -> StartTagOutcome {
match tag.tag() {
"code" => {
if !writer.is_inside("pre") {
writer.push_str("`");
}
}
"div" => {
let classes = tag.classes();
self.language = classes.iter().find_map(|class| {
if let Some((_, language)) = class.split_once("mw-highlight-lang-") {
Some(language.trim().to_owned())
} else {
None
}
});
}
"pre" => {
writer.push_blank_line();
writer.push_str("```");
if let Some(language) = self.language.take() {
writer.push_str(&language);
}
writer.push_newline();
}
_ => {}
}
StartTagOutcome::Continue
}
fn handle_tag_end(&mut self, tag: &HtmlElement, writer: &mut MarkdownWriter) {
match tag.tag() {
"code" => {
if !writer.is_inside("pre") {
writer.push_str("`");
}
}
"pre" => writer.push_str("\n```\n"),
_ => {}
}
}
fn handle_text(&mut self, text: &str, writer: &mut MarkdownWriter) -> HandlerOutcome {
if writer.is_inside("pre") {
writer.push_str(text);
return HandlerOutcome::Handled;
}
HandlerOutcome::NoOp
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::rc::Rc;
use indoc::indoc;
use pretty_assertions::assert_eq;
use crate::{TagHandler, convert_html_to_markdown, markdown};
use super::*;
fn wikipedia_handlers() -> Vec<TagHandler> {
vec![
Rc::new(RefCell::new(markdown::ParagraphHandler)),
Rc::new(RefCell::new(markdown::HeadingHandler)),
Rc::new(RefCell::new(markdown::ListHandler)),
Rc::new(RefCell::new(markdown::StyledTextHandler)),
Rc::new(RefCell::new(WikipediaChromeRemover)),
]
}
#[test]
fn test_citation_references_get_removed() {
let html = indoc! {r##"
<p>Rust began as a personal project in 2006 by <a href="/wiki/Mozilla" title="Mozilla">Mozilla</a> Research employee Graydon Hoare.<sup id="cite_ref-MITTechReview_23-0" class="reference"><a href="#cite_note-MITTechReview-23">[20]</a></sup> Mozilla began sponsoring the project in 2009 as a part of the ongoing development of an experimental <a href="/wiki/Browser_engine" title="Browser engine">browser engine</a> called <a href="/wiki/Servo_(software)" title="Servo (software)">Servo</a>,<sup id="cite_ref-infoq2012_24-0" class="reference"><a href="#cite_note-infoq2012-24">[21]</a></sup> which was officially announced by Mozilla in 2010.<sup id="cite_ref-MattAsay_25-0" class="reference"><a href="#cite_note-MattAsay-25">[22]</a></sup><sup id="cite_ref-26" class="reference"><a href="#cite_note-26">[23]</a></sup> Rust's memory and ownership system was influenced by <a href="/wiki/Region-based_memory_management" title="Region-based memory management">region-based memory management</a> in languages such as <a href="/wiki/Cyclone_(programming_language)" title="Cyclone (programming language)">Cyclone</a> and ML Kit.<sup id="cite_ref-influences_8-13" class="reference"><a href="#cite_note-influences-8">[5]</a></sup>
</p>
"##};
let expected = indoc! {"
Rust began as a personal project in 2006 by Mozilla Research employee Graydon Hoare. Mozilla began sponsoring the project in 2009 as a part of the ongoing development of an experimental browser engine called Servo, which was officially announced by Mozilla in 2010. Rust's memory and ownership system was influenced by region-based memory management in languages such as Cyclone and ML Kit.
"}
.trim();
assert_eq!(
convert_html_to_markdown(html.as_bytes(), &mut wikipedia_handlers()).unwrap(),
expected
)
}
}
-37
View File
@@ -1,37 +0,0 @@
[package]
name = "http_client"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "A HTTP client library for Zed and GPUI"
[lints]
workspace = true
[features]
test-support = []
[lib]
path = "src/http_client.rs"
doctest = true
[dependencies]
anyhow.workspace = true
async-compression.workspace = true
async-fs.workspace = true
async-tar.workspace = true
bytes.workspace = true
derive_more.workspace = true
futures.workspace = true
http-body.workspace = true
http.workspace = true
log.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_urlencoded.workspace = true
sha2.workspace = true
tempfile.workspace = true
url.workspace = true
util.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-138
View File
@@ -1,138 +0,0 @@
use std::{
io::{Cursor, Read},
pin::Pin,
task::Poll,
};
use bytes::Bytes;
use futures::AsyncRead;
use http_body::{Body, Frame};
/// Based on the implementation of AsyncBody in
/// <https://github.com/sagebind/isahc/blob/5c533f1ef4d6bdf1fd291b5103c22110f41d0bf0/src/body/mod.rs>.
pub struct AsyncBody(pub Inner);
pub enum Inner {
/// An empty body.
Empty,
/// A body stored in memory.
Bytes(std::io::Cursor<Bytes>),
/// An asynchronous reader.
AsyncReader(Pin<Box<dyn futures::AsyncRead + Send + Sync>>),
}
impl AsyncBody {
/// Create a new empty body.
///
/// An empty body represents the *absence* of a body, which is semantically
/// different than the presence of a body of zero length.
pub fn empty() -> Self {
Self(Inner::Empty)
}
/// Create a streaming body that reads from the given reader.
pub fn from_reader<R>(read: R) -> Self
where
R: AsyncRead + Send + Sync + 'static,
{
Self(Inner::AsyncReader(Box::pin(read)))
}
pub fn from_bytes(bytes: Bytes) -> Self {
Self(Inner::Bytes(Cursor::new(bytes)))
}
}
impl Default for AsyncBody {
fn default() -> Self {
Self(Inner::Empty)
}
}
impl From<()> for AsyncBody {
fn from(_: ()) -> Self {
Self(Inner::Empty)
}
}
impl From<Bytes> for AsyncBody {
fn from(bytes: Bytes) -> Self {
Self::from_bytes(bytes)
}
}
impl From<Vec<u8>> for AsyncBody {
fn from(body: Vec<u8>) -> Self {
Self::from_bytes(body.into())
}
}
impl From<String> for AsyncBody {
fn from(body: String) -> Self {
Self::from_bytes(body.into())
}
}
impl From<&'static [u8]> for AsyncBody {
#[inline]
fn from(s: &'static [u8]) -> Self {
Self::from_bytes(Bytes::from_static(s))
}
}
impl From<&'static str> for AsyncBody {
#[inline]
fn from(s: &'static str) -> Self {
Self::from_bytes(Bytes::from_static(s.as_bytes()))
}
}
impl<T: Into<Self>> From<Option<T>> for AsyncBody {
fn from(body: Option<T>) -> Self {
match body {
Some(body) => body.into(),
None => Self::empty(),
}
}
}
impl futures::AsyncRead for AsyncBody {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut [u8],
) -> std::task::Poll<std::io::Result<usize>> {
// SAFETY: Standard Enum pin projection
let inner = unsafe { &mut self.get_unchecked_mut().0 };
match inner {
Inner::Empty => Poll::Ready(Ok(0)),
// Blocking call is over an in-memory buffer
Inner::Bytes(cursor) => Poll::Ready(cursor.read(buf)),
Inner::AsyncReader(async_reader) => {
AsyncRead::poll_read(async_reader.as_mut(), cx, buf)
}
}
}
}
impl Body for AsyncBody {
type Data = Bytes;
type Error = std::io::Error;
fn poll_frame(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
let mut buffer = vec![0; 8192];
match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) {
Poll::Ready(Ok(0)) => Poll::Ready(None),
Poll::Ready(Ok(n)) => {
let data = Bytes::copy_from_slice(&buffer[..n]);
Poll::Ready(Some(Ok(Frame::data(data))))
}
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))),
Poll::Pending => Poll::Pending,
}
}
}
-192
View File
@@ -1,192 +0,0 @@
use crate::{HttpClient, HttpRequestExt};
use anyhow::{Context as _, Result, anyhow, bail};
use futures::AsyncReadExt;
use http::Request;
use serde::Deserialize;
use std::sync::Arc;
use url::Url;
const GITHUB_API_URL: &str = "https://api.github.com";
pub struct GitHubLspBinaryVersion {
pub name: String,
pub url: String,
pub digest: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct GithubRelease {
pub tag_name: String,
#[serde(rename = "prerelease")]
pub pre_release: bool,
pub assets: Vec<GithubReleaseAsset>,
pub tarball_url: String,
pub zipball_url: String,
}
#[derive(Deserialize, Debug)]
pub struct GithubReleaseAsset {
pub name: String,
pub browser_download_url: String,
pub digest: Option<String>,
}
pub async fn latest_github_release(
repo_name_with_owner: &str,
require_assets: bool,
pre_release: bool,
http: Arc<dyn HttpClient>,
) -> anyhow::Result<GithubRelease> {
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases");
let request = Request::get(&url)
.follow_redirects(crate::RedirectPolicy::FollowAll)
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
builder.header("Authorization", format!("Bearer {}", token))
})
.body(Default::default())?;
let mut response = http
.send(request)
.await
.context("error fetching latest release")?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading latest release")?;
if response.status().is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let releases = match serde_json::from_slice::<Vec<GithubRelease>>(body.as_slice()) {
Ok(releases) => releases,
Err(err) => {
log::error!("Error deserializing: {err:?}");
log::error!(
"GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice())
);
anyhow::bail!("error deserializing latest release: {err:?}");
}
};
let mut release = releases
.into_iter()
.filter(|release| !require_assets || !release.assets.is_empty())
.find(|release| release.pre_release == pre_release)
.context("finding a prerelease")?;
release.assets.iter_mut().for_each(|asset| {
if let Some(digest) = &mut asset.digest
&& let Some(stripped) = digest.strip_prefix("sha256:")
{
*digest = stripped.to_owned();
}
});
Ok(release)
}
pub async fn get_release_by_tag_name(
repo_name_with_owner: &str,
tag: &str,
http: Arc<dyn HttpClient>,
) -> anyhow::Result<GithubRelease> {
let url = format!("{GITHUB_API_URL}/repos/{repo_name_with_owner}/releases/tags/{tag}");
let request = Request::get(&url)
.follow_redirects(crate::RedirectPolicy::FollowAll)
.when_some(std::env::var("GITHUB_TOKEN").ok(), |builder, token| {
builder.header("Authorization", format!("Bearer {}", token))
})
.body(Default::default())?;
let mut response = http
.send(request)
.await
.context("error fetching latest release")?;
let mut body = Vec::new();
let status = response.status();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading latest release")?;
if status.is_client_error() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
let release = serde_json::from_slice::<GithubRelease>(body.as_slice()).map_err(|err| {
log::error!("Error deserializing: {err:?}");
log::error!(
"GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice())
);
anyhow!("error deserializing GitHub release: {err:?}")
})?;
Ok(release)
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum AssetKind {
TarGz,
Gz,
Zip,
}
pub fn build_asset_url(repo_name_with_owner: &str, tag: &str, kind: AssetKind) -> Result<String> {
let mut url = Url::parse(&format!(
"https://github.com/{repo_name_with_owner}/archive/refs/tags",
))?;
// We're pushing this here, because tags may contain `/` and other characters
// that need to be escaped.
let asset_filename = format!(
"{tag}.{extension}",
extension = match kind {
AssetKind::TarGz => "tar.gz",
AssetKind::Gz => "gz",
AssetKind::Zip => "zip",
}
);
url.path_segments_mut()
.map_err(|()| anyhow!("cannot modify url path segments"))?
.push(&asset_filename);
Ok(url.to_string())
}
#[cfg(test)]
mod tests {
use crate::github::{AssetKind, build_asset_url};
#[test]
fn test_build_asset_url() {
let tag = "release/2.3.5";
let repo_name_with_owner = "microsoft/vscode-eslint";
let tarball = build_asset_url(repo_name_with_owner, tag, AssetKind::TarGz).unwrap();
assert_eq!(
tarball,
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.tar.gz"
);
let zip = build_asset_url(repo_name_with_owner, tag, AssetKind::Zip).unwrap();
assert_eq!(
zip,
"https://github.com/microsoft/vscode-eslint/archive/refs/tags/release%2F2.3.5.zip"
);
}
}
-189
View File
@@ -1,189 +0,0 @@
use std::{path::Path, pin::Pin, task::Poll};
use anyhow::{Context, Result};
use async_compression::futures::bufread::GzipDecoder;
use futures::{AsyncRead, AsyncSeek, AsyncSeekExt, AsyncWrite, io::BufReader};
use sha2::{Digest, Sha256};
use crate::{HttpClient, github::AssetKind};
#[derive(serde::Deserialize, serde::Serialize, Debug)]
pub struct GithubBinaryMetadata {
pub metadata_version: u64,
pub digest: Option<String>,
}
impl GithubBinaryMetadata {
pub async fn read_from_file(metadata_path: &Path) -> Result<GithubBinaryMetadata> {
let metadata_content = async_fs::read_to_string(metadata_path)
.await
.with_context(|| format!("reading metadata file at {metadata_path:?}"))?;
serde_json::from_str(&metadata_content)
.with_context(|| format!("parsing metadata file at {metadata_path:?}"))
}
pub async fn write_to_file(&self, metadata_path: &Path) -> Result<()> {
let metadata_content = serde_json::to_string(self)
.with_context(|| format!("serializing metadata for {metadata_path:?}"))?;
async_fs::write(metadata_path, metadata_content.as_bytes())
.await
.with_context(|| format!("writing metadata file at {metadata_path:?}"))?;
Ok(())
}
}
pub async fn download_server_binary(
http_client: &dyn HttpClient,
url: &str,
digest: Option<&str>,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<(), anyhow::Error> {
log::info!("downloading github artifact from {url}");
let mut response = http_client
.get(url, Default::default(), true)
.await
.with_context(|| format!("downloading release from {url}"))?;
let body = response.body_mut();
match digest {
Some(expected_sha_256) => {
let temp_asset_file = tempfile::NamedTempFile::new()
.with_context(|| format!("creating a temporary file for {url}"))?;
let (temp_asset_file, _temp_guard) = temp_asset_file.into_parts();
let mut writer = HashingWriter {
writer: async_fs::File::from(temp_asset_file),
hasher: Sha256::new(),
};
futures::io::copy(&mut BufReader::new(body), &mut writer)
.await
.with_context(|| {
format!("saving archive contents into the temporary file for {url}",)
})?;
let asset_sha_256 = format!("{:x}", writer.hasher.finalize());
anyhow::ensure!(
asset_sha_256 == expected_sha_256,
"{url} asset got SHA-256 mismatch. Expected: {expected_sha_256}, Got: {asset_sha_256}",
);
writer
.writer
.seek(std::io::SeekFrom::Start(0))
.await
.with_context(|| format!("seeking temporary file {destination_path:?}",))?;
stream_file_archive(&mut writer.writer, url, destination_path, asset_kind)
.await
.with_context(|| {
format!("extracting downloaded asset for {url} into {destination_path:?}",)
})?;
}
None => stream_response_archive(body, url, destination_path, asset_kind)
.await
.with_context(|| {
format!("extracting response for asset {url} into {destination_path:?}",)
})?,
}
Ok(())
}
async fn stream_response_archive(
response: impl AsyncRead + Unpin,
url: &str,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, response).await?,
AssetKind::Gz => extract_gz(destination_path, url, response).await?,
AssetKind::Zip => {
util::archive::extract_zip(destination_path, response).await?;
}
};
Ok(())
}
async fn stream_file_archive(
file_archive: impl AsyncRead + AsyncSeek + Unpin,
url: &str,
destination_path: &Path,
asset_kind: AssetKind,
) -> Result<()> {
match asset_kind {
AssetKind::TarGz => extract_tar_gz(destination_path, url, file_archive).await?,
AssetKind::Gz => extract_gz(destination_path, url, file_archive).await?,
#[cfg(not(windows))]
AssetKind::Zip => {
util::archive::extract_seekable_zip(destination_path, file_archive).await?;
}
#[cfg(windows)]
AssetKind::Zip => {
util::archive::extract_zip(destination_path, file_archive).await?;
}
};
Ok(())
}
async fn extract_tar_gz(
destination_path: &Path,
url: &str,
from: impl AsyncRead + Unpin,
) -> Result<(), anyhow::Error> {
let decompressed_bytes = GzipDecoder::new(BufReader::new(from));
let archive = async_tar::Archive::new(decompressed_bytes);
archive
.unpack(&destination_path)
.await
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
Ok(())
}
async fn extract_gz(
destination_path: &Path,
url: &str,
from: impl AsyncRead + Unpin,
) -> Result<(), anyhow::Error> {
let mut decompressed_bytes = GzipDecoder::new(BufReader::new(from));
let mut file = async_fs::File::create(&destination_path)
.await
.with_context(|| {
format!("creating a file {destination_path:?} for a download from {url}")
})?;
futures::io::copy(&mut decompressed_bytes, &mut file)
.await
.with_context(|| format!("extracting {url} to {destination_path:?}"))?;
Ok(())
}
struct HashingWriter<W: AsyncWrite + Unpin> {
writer: W,
hasher: Sha256,
}
impl<W: AsyncWrite + Unpin> AsyncWrite for HashingWriter<W> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<std::result::Result<usize, std::io::Error>> {
match Pin::new(&mut self.writer).poll_write(cx, buf) {
Poll::Ready(Ok(n)) => {
self.hasher.update(&buf[..n]);
Poll::Ready(Ok(n))
}
other => other,
}
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.writer).poll_flush(cx)
}
fn poll_close(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<std::result::Result<(), std::io::Error>> {
Pin::new(&mut self.writer).poll_close(cx)
}
}
-386
View File
@@ -1,386 +0,0 @@
mod async_body;
pub mod github;
pub mod github_download;
pub use anyhow::{Result, anyhow};
pub use async_body::{AsyncBody, Inner};
use derive_more::Deref;
use http::HeaderValue;
pub use http::{self, Method, Request, Response, StatusCode, Uri, request::Builder};
use futures::future::BoxFuture;
use parking_lot::Mutex;
use std::sync::Arc;
#[cfg(feature = "test-support")]
use std::{any::type_name, fmt};
pub use url::{Host, Url};
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
pub enum RedirectPolicy {
#[default]
NoFollow,
FollowLimit(u32),
FollowAll,
}
pub struct FollowRedirects(pub bool);
pub trait HttpRequestExt {
/// Conditionally modify self with the given closure.
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
where
Self: Sized,
{
if condition { then(self) } else { self }
}
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,
{
match option {
Some(value) => then(self, value),
None => self,
}
}
/// Whether or not to follow redirects
fn follow_redirects(self, follow: RedirectPolicy) -> Self;
}
impl HttpRequestExt for http::request::Builder {
fn follow_redirects(self, follow: RedirectPolicy) -> Self {
self.extension(follow)
}
}
pub trait HttpClient: 'static + Send + Sync {
fn user_agent(&self) -> Option<&HeaderValue>;
fn proxy(&self) -> Option<&Url>;
fn send(
&self,
req: http::Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>;
fn get(
&self,
uri: &str,
body: AsyncBody,
follow_redirects: bool,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
let request = Builder::new()
.uri(uri)
.follow_redirects(if follow_redirects {
RedirectPolicy::FollowAll
} else {
RedirectPolicy::NoFollow
})
.body(body);
match request {
Ok(request) => self.send(request),
Err(e) => Box::pin(async move { Err(e.into()) }),
}
}
fn post_json(
&self,
uri: &str,
body: AsyncBody,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
let request = Builder::new()
.uri(uri)
.method(Method::POST)
.header("Content-Type", "application/json")
.body(body);
match request {
Ok(request) => self.send(request),
Err(e) => Box::pin(async move { Err(e.into()) }),
}
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
}
}
/// An [`HttpClient`] that may have a proxy.
#[derive(Deref)]
pub struct HttpClientWithProxy {
#[deref]
client: Arc<dyn HttpClient>,
proxy: Option<Url>,
}
impl HttpClientWithProxy {
/// Returns a new [`HttpClientWithProxy`] with the given proxy URL.
pub fn new(client: Arc<dyn HttpClient>, proxy_url: Option<String>) -> Self {
let proxy_url = proxy_url
.and_then(|proxy| proxy.parse().ok())
.or_else(read_proxy_from_env);
Self::new_url(client, proxy_url)
}
pub fn new_url(client: Arc<dyn HttpClient>, proxy_url: Option<Url>) -> Self {
Self {
client,
proxy: proxy_url,
}
}
}
impl HttpClient for HttpClientWithProxy {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
self.client.send(req)
}
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> {
self.proxy.as_ref()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
}
/// An [`HttpClient`] that has a base URL.
#[derive(Deref)]
pub struct HttpClientWithUrl {
base_url: Mutex<String>,
#[deref]
client: HttpClientWithProxy,
}
impl HttpClientWithUrl {
/// Returns a new [`HttpClientWithUrl`] with the given base URL.
pub fn new(
client: Arc<dyn HttpClient>,
base_url: impl Into<String>,
proxy_url: Option<String>,
) -> Self {
let client = HttpClientWithProxy::new(client, proxy_url);
Self {
base_url: Mutex::new(base_url.into()),
client,
}
}
pub fn new_url(
client: Arc<dyn HttpClient>,
base_url: impl Into<String>,
proxy_url: Option<Url>,
) -> Self {
let client = HttpClientWithProxy::new_url(client, proxy_url);
Self {
base_url: Mutex::new(base_url.into()),
client,
}
}
/// Returns the base URL.
pub fn base_url(&self) -> String {
self.base_url.lock().clone()
}
/// Sets the base URL.
pub fn set_base_url(&self, base_url: impl Into<String>) {
let base_url = base_url.into();
*self.base_url.lock() = base_url;
}
/// Builds a URL using the given path.
pub fn build_url(&self, path: &str) -> String {
format!("{}{}", self.base_url(), path)
}
}
impl HttpClient for HttpClientWithUrl {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
self.client.send(req)
}
fn user_agent(&self) -> Option<&HeaderValue> {
self.client.user_agent()
}
fn proxy(&self) -> Option<&Url> {
self.client.proxy.as_ref()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
}
pub fn read_proxy_from_env() -> Option<Url> {
const ENV_VARS: &[&str] = &[
"ALL_PROXY",
"all_proxy",
"HTTPS_PROXY",
"https_proxy",
"HTTP_PROXY",
"http_proxy",
];
ENV_VARS
.iter()
.find_map(|var| std::env::var(var).ok())
.and_then(|env| env.parse().ok())
}
pub fn read_no_proxy_from_env() -> Option<String> {
const ENV_VARS: &[&str] = &["NO_PROXY", "no_proxy"];
ENV_VARS.iter().find_map(|var| std::env::var(var).ok())
}
pub struct BlockedHttpClient;
impl BlockedHttpClient {
pub fn new() -> Self {
BlockedHttpClient
}
}
impl HttpClient for BlockedHttpClient {
fn send(
&self,
_req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
Box::pin(async {
Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"BlockedHttpClient disallowed request",
)
.into())
})
}
fn user_agent(&self) -> Option<&HeaderValue> {
None
}
fn proxy(&self) -> Option<&Url> {
None
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
}
}
#[cfg(feature = "test-support")]
type FakeHttpHandler = Arc<
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
+ Send
+ Sync
+ 'static,
>;
#[cfg(feature = "test-support")]
pub struct FakeHttpClient {
handler: Mutex<Option<FakeHttpHandler>>,
user_agent: HeaderValue,
}
#[cfg(feature = "test-support")]
impl FakeHttpClient {
pub fn create<Fut, F>(handler: F) -> Arc<HttpClientWithUrl>
where
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
F: Fn(Request<AsyncBody>) -> Fut + Send + Sync + 'static,
{
Arc::new(HttpClientWithUrl {
base_url: Mutex::new("http://test.example".into()),
client: HttpClientWithProxy {
client: Arc::new(Self {
handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
user_agent: HeaderValue::from_static(type_name::<Self>()),
}),
proxy: None,
},
})
}
pub fn with_404_response() -> Arc<HttpClientWithUrl> {
log::warn!("Using fake HTTP client with 404 response");
Self::create(|_| async move {
Ok(Response::builder()
.status(404)
.body(Default::default())
.unwrap())
})
}
pub fn with_200_response() -> Arc<HttpClientWithUrl> {
log::warn!("Using fake HTTP client with 200 response");
Self::create(|_| async move {
Ok(Response::builder()
.status(200)
.body(Default::default())
.unwrap())
})
}
pub fn replace_handler<Fut, F>(&self, new_handler: F)
where
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
{
let mut handler = self.handler.lock();
let old_handler = handler.take().unwrap();
*handler = Some(Arc::new(move |req| {
Box::pin(new_handler(old_handler.clone(), req))
}));
}
}
#[cfg(feature = "test-support")]
impl fmt::Debug for FakeHttpClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FakeHttpClient").finish()
}
}
#[cfg(feature = "test-support")]
impl HttpClient for FakeHttpClient {
fn send(
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
((self.handler.lock().as_ref().unwrap())(req)) as _
}
fn user_agent(&self) -> Option<&HeaderValue> {
Some(&self.user_agent)
}
fn proxy(&self) -> Option<&Url> {
None
}
fn as_fake(&self) -> &FakeHttpClient {
self
}
}
-20
View File
@@ -1,20 +0,0 @@
[package]
name = "http_client_tls"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[features]
test-support = []
[lib]
path = "src/http_client_tls.rs"
doctest = true
[dependencies]
rustls.workspace = true
rustls-platform-verifier.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
@@ -1,21 +0,0 @@
use std::sync::OnceLock;
use rustls::ClientConfig;
use rustls_platform_verifier::ConfigVerifierExt;
static TLS_CONFIG: OnceLock<rustls::ClientConfig> = OnceLock::new();
pub fn tls_config() -> ClientConfig {
TLS_CONFIG
.get_or_init(|| {
// rustls uses the `aws_lc_rs` provider by default
// This only errors if the default provider has already
// been installed. We can ignore this `Result`.
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
ClientConfig::with_platform_verifier()
})
.clone()
}
-28
View File
@@ -1,28 +0,0 @@
[package]
name = "media"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "Bindings to macos media handling APIs for Zed"
[lints]
workspace = true
[lib]
path = "src/media.rs"
doctest = false
[dependencies]
anyhow.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true
ctor.workspace = true
foreign-types = "0.5"
metal.workspace = true
core-video.workspace = true
objc.workspace = true
[build-dependencies]
bindgen = "0.71"
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-44
View File
@@ -1,44 +0,0 @@
#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")]
#[cfg(target_os = "macos")]
fn main() {
use std::{env, path::PathBuf, process::Command};
let sdk_path = String::from_utf8(
Command::new("xcrun")
.args(["--sdk", "macosx", "--show-sdk-path"])
.output()
.unwrap()
.stdout,
)
.unwrap();
let sdk_path = sdk_path.trim_end();
println!("cargo:rerun-if-changed=src/bindings.h");
let bindings = bindgen::Builder::default()
.header("src/bindings.h")
.clang_arg(format!("-isysroot{}", sdk_path))
.clang_arg("-xobjective-c")
.allowlist_type("CMItemIndex")
.allowlist_type("CMSampleTimingInfo")
.allowlist_type("CMVideoCodecType")
.allowlist_type("VTEncodeInfoFlags")
.allowlist_function("CMTimeMake")
.allowlist_var("kCVPixelFormatType_.*")
.allowlist_var("kCVReturn.*")
.allowlist_var("VTEncodeInfoFlags_.*")
.allowlist_var("kCMVideoCodecType_.*")
.allowlist_var("kCMTime.*")
.allowlist_var("kCMSampleAttachmentKey_.*")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.layout_tests(false)
.generate()
.expect("unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("couldn't write dispatch bindings");
}
#[cfg(not(target_os = "macos"))]
fn main() {}
-5
View File
@@ -1,5 +0,0 @@
#import <CoreMedia/CMFormatDescription.h>
#import <CoreMedia/CMSampleBuffer.h>
#import <CoreVideo/CVPixelFormatDescription.h>
#import <CoreVideo/CVReturn.h>
#import <VideoToolbox/VTCompressionSession.h>
-10
View File
@@ -1,10 +0,0 @@
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(unused)]
#[cfg(target_os = "macos")]
use objc::*;
#[cfg(target_os = "macos")]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
-352
View File
@@ -1,352 +0,0 @@
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
mod bindings;
#[cfg(target_os = "macos")]
pub mod core_media {
#![allow(non_snake_case)]
pub use crate::bindings::{
CMItemIndex, CMSampleTimingInfo, CMTime, CMTimeMake, CMVideoCodecType,
kCMSampleAttachmentKey_NotSync, kCMTimeInvalid, kCMVideoCodecType_H264,
};
use anyhow::Result;
use core_foundation::{
array::{CFArray, CFArrayRef},
base::{CFTypeID, OSStatus, TCFType},
declare_TCFType,
dictionary::CFDictionary,
impl_CFTypeDescription, impl_TCFType,
string::CFString,
};
use core_video::image_buffer::{CVImageBuffer, CVImageBufferRef};
use std::{ffi::c_void, ptr};
#[repr(C)]
pub struct __CMSampleBuffer(c_void);
// The ref type must be a pointer to the underlying struct.
pub type CMSampleBufferRef = *const __CMSampleBuffer;
declare_TCFType!(CMSampleBuffer, CMSampleBufferRef);
impl_TCFType!(CMSampleBuffer, CMSampleBufferRef, CMSampleBufferGetTypeID);
impl_CFTypeDescription!(CMSampleBuffer);
impl CMSampleBuffer {
pub fn attachments(&self) -> Vec<CFDictionary<CFString>> {
unsafe {
let attachments =
CMSampleBufferGetSampleAttachmentsArray(self.as_concrete_TypeRef(), true);
CFArray::<CFDictionary>::wrap_under_get_rule(attachments)
.into_iter()
.map(|attachments| {
CFDictionary::wrap_under_get_rule(attachments.as_concrete_TypeRef())
})
.collect()
}
}
pub fn image_buffer(&self) -> Option<CVImageBuffer> {
unsafe {
let ptr = CMSampleBufferGetImageBuffer(self.as_concrete_TypeRef());
if ptr.is_null() {
None
} else {
Some(CVImageBuffer::wrap_under_get_rule(ptr))
}
}
}
pub fn sample_timing_info(&self, index: usize) -> Result<CMSampleTimingInfo> {
unsafe {
let mut timing_info = CMSampleTimingInfo {
duration: kCMTimeInvalid,
presentationTimeStamp: kCMTimeInvalid,
decodeTimeStamp: kCMTimeInvalid,
};
let result = CMSampleBufferGetSampleTimingInfo(
self.as_concrete_TypeRef(),
index as CMItemIndex,
&mut timing_info,
);
anyhow::ensure!(
result == 0,
"error getting sample timing info, code {result}"
);
Ok(timing_info)
}
}
pub fn format_description(&self) -> CMFormatDescription {
unsafe {
CMFormatDescription::wrap_under_get_rule(CMSampleBufferGetFormatDescription(
self.as_concrete_TypeRef(),
))
}
}
pub fn data(&self) -> CMBlockBuffer {
unsafe {
CMBlockBuffer::wrap_under_get_rule(CMSampleBufferGetDataBuffer(
self.as_concrete_TypeRef(),
))
}
}
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMSampleBufferGetTypeID() -> CFTypeID;
fn CMSampleBufferGetSampleAttachmentsArray(
buffer: CMSampleBufferRef,
create_if_necessary: bool,
) -> CFArrayRef;
fn CMSampleBufferGetImageBuffer(buffer: CMSampleBufferRef) -> CVImageBufferRef;
fn CMSampleBufferGetSampleTimingInfo(
buffer: CMSampleBufferRef,
index: CMItemIndex,
timing_info_out: *mut CMSampleTimingInfo,
) -> OSStatus;
fn CMSampleBufferGetFormatDescription(buffer: CMSampleBufferRef) -> CMFormatDescriptionRef;
fn CMSampleBufferGetDataBuffer(sample_buffer: CMSampleBufferRef) -> CMBlockBufferRef;
}
#[repr(C)]
pub struct __CMFormatDescription(c_void);
pub type CMFormatDescriptionRef = *const __CMFormatDescription;
declare_TCFType!(CMFormatDescription, CMFormatDescriptionRef);
impl_TCFType!(
CMFormatDescription,
CMFormatDescriptionRef,
CMFormatDescriptionGetTypeID
);
impl_CFTypeDescription!(CMFormatDescription);
impl CMFormatDescription {
pub fn h264_parameter_set_count(&self) -> usize {
unsafe {
let mut count = 0;
let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
self.as_concrete_TypeRef(),
0,
ptr::null_mut(),
ptr::null_mut(),
&mut count,
ptr::null_mut(),
);
assert_eq!(result, 0);
count
}
}
pub fn h264_parameter_set_at_index(&self, index: usize) -> Result<&[u8]> {
unsafe {
let mut bytes = ptr::null();
let mut len = 0;
let result = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
self.as_concrete_TypeRef(),
index,
&mut bytes,
&mut len,
ptr::null_mut(),
ptr::null_mut(),
);
anyhow::ensure!(result == 0, "error getting parameter set, code: {result}");
Ok(std::slice::from_raw_parts(bytes, len))
}
}
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMFormatDescriptionGetTypeID() -> CFTypeID;
fn CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
video_desc: CMFormatDescriptionRef,
parameter_set_index: usize,
parameter_set_pointer_out: *mut *const u8,
parameter_set_size_out: *mut usize,
parameter_set_count_out: *mut usize,
NALUnitHeaderLengthOut: *mut isize,
) -> OSStatus;
}
#[repr(C)]
pub struct __CMBlockBuffer(c_void);
pub type CMBlockBufferRef = *const __CMBlockBuffer;
declare_TCFType!(CMBlockBuffer, CMBlockBufferRef);
impl_TCFType!(CMBlockBuffer, CMBlockBufferRef, CMBlockBufferGetTypeID);
impl_CFTypeDescription!(CMBlockBuffer);
impl CMBlockBuffer {
pub fn bytes(&self) -> &[u8] {
unsafe {
let mut bytes = ptr::null();
let mut len = 0;
let result = CMBlockBufferGetDataPointer(
self.as_concrete_TypeRef(),
0,
&mut 0,
&mut len,
&mut bytes,
);
assert!(result == 0, "could not get block buffer data");
std::slice::from_raw_parts(bytes, len)
}
}
}
#[link(name = "CoreMedia", kind = "framework")]
unsafe extern "C" {
fn CMBlockBufferGetTypeID() -> CFTypeID;
fn CMBlockBufferGetDataPointer(
buffer: CMBlockBufferRef,
offset: usize,
length_at_offset_out: *mut usize,
total_length_out: *mut usize,
data_pointer_out: *mut *const u8,
) -> OSStatus;
}
}
#[cfg(target_os = "macos")]
pub mod core_video {
#![allow(non_snake_case)]
#[cfg(target_os = "macos")]
use core_foundation::{
base::{CFTypeID, TCFType},
declare_TCFType, impl_CFTypeDescription, impl_TCFType,
};
#[cfg(target_os = "macos")]
use std::ffi::c_void;
use crate::bindings::{CVReturn, kCVReturnSuccess};
pub use crate::bindings::{
kCVPixelFormatType_32BGRA, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, kCVPixelFormatType_420YpCbCr8Planar,
};
use anyhow::Result;
use core_foundation::{
base::kCFAllocatorDefault, dictionary::CFDictionaryRef, mach_port::CFAllocatorRef,
};
use foreign_types::ForeignTypeRef;
use metal::{MTLDevice, MTLPixelFormat};
use std::ptr;
#[repr(C)]
pub struct __CVMetalTextureCache(c_void);
pub type CVMetalTextureCacheRef = *const __CVMetalTextureCache;
declare_TCFType!(CVMetalTextureCache, CVMetalTextureCacheRef);
impl_TCFType!(
CVMetalTextureCache,
CVMetalTextureCacheRef,
CVMetalTextureCacheGetTypeID
);
impl_CFTypeDescription!(CVMetalTextureCache);
impl CVMetalTextureCache {
/// # Safety
///
/// metal_device must be valid according to CVMetalTextureCacheCreate
pub unsafe fn new(metal_device: *mut MTLDevice) -> Result<Self> {
let mut this = ptr::null();
let result = unsafe {
CVMetalTextureCacheCreate(
kCFAllocatorDefault,
ptr::null(),
metal_device,
ptr::null(),
&mut this,
)
};
anyhow::ensure!(
result == kCVReturnSuccess,
"could not create texture cache, code: {result}"
);
unsafe { Ok(CVMetalTextureCache::wrap_under_create_rule(this)) }
}
/// # Safety
///
/// The arguments to this function must be valid according to CVMetalTextureCacheCreateTextureFromImage
pub unsafe fn create_texture_from_image(
&self,
source: ::core_video::image_buffer::CVImageBufferRef,
texture_attributes: CFDictionaryRef,
pixel_format: MTLPixelFormat,
width: usize,
height: usize,
plane_index: usize,
) -> Result<CVMetalTexture> {
let mut this = ptr::null();
let result = unsafe {
CVMetalTextureCacheCreateTextureFromImage(
kCFAllocatorDefault,
self.as_concrete_TypeRef(),
source,
texture_attributes,
pixel_format,
width,
height,
plane_index,
&mut this,
)
};
anyhow::ensure!(
result == kCVReturnSuccess,
"could not create texture, code: {result}"
);
unsafe { Ok(CVMetalTexture::wrap_under_create_rule(this)) }
}
}
#[link(name = "CoreVideo", kind = "framework")]
unsafe extern "C" {
fn CVMetalTextureCacheGetTypeID() -> CFTypeID;
fn CVMetalTextureCacheCreate(
allocator: CFAllocatorRef,
cache_attributes: CFDictionaryRef,
metal_device: *const MTLDevice,
texture_attributes: CFDictionaryRef,
cache_out: *mut CVMetalTextureCacheRef,
) -> CVReturn;
fn CVMetalTextureCacheCreateTextureFromImage(
allocator: CFAllocatorRef,
texture_cache: CVMetalTextureCacheRef,
source_image: ::core_video::image_buffer::CVImageBufferRef,
texture_attributes: CFDictionaryRef,
pixel_format: MTLPixelFormat,
width: usize,
height: usize,
plane_index: usize,
texture_out: *mut CVMetalTextureRef,
) -> CVReturn;
}
#[repr(C)]
pub struct __CVMetalTexture(c_void);
pub type CVMetalTextureRef = *const __CVMetalTexture;
declare_TCFType!(CVMetalTexture, CVMetalTextureRef);
impl_TCFType!(CVMetalTexture, CVMetalTextureRef, CVMetalTextureGetTypeID);
impl_CFTypeDescription!(CVMetalTexture);
impl CVMetalTexture {
pub fn as_texture_ref(&self) -> &metal::TextureRef {
unsafe {
let texture = CVMetalTextureGetTexture(self.as_concrete_TypeRef());
metal::TextureRef::from_ptr(texture as *mut _)
}
}
}
#[link(name = "CoreVideo", kind = "framework")]
unsafe extern "C" {
fn CVMetalTextureGetTypeID() -> CFTypeID;
fn CVMetalTextureGetTexture(texture: CVMetalTextureRef) -> *mut c_void;
}
}
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "refineable"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "A macro for creating 'refinement' types that can be used to partially initialize or mutate a complex struct"
[lints]
workspace = true
[lib]
path = "src/refineable.rs"
doctest = false
[dependencies]
derive_refineable.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
@@ -1,21 +0,0 @@
[package]
name = "derive_refineable"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "A derive macro for creating refinement types in Rust"
[lints]
workspace = true
[lib]
path = "src/derive_refineable.rs"
proc-macro = true
doctest = false
[dependencies]
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
@@ -1 +0,0 @@
../../../LICENSE-APACHE
@@ -1,543 +0,0 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
DeriveInput, Field, FieldsNamed, PredicateType, TraitBound, Type, TypeParamBound, WhereClause,
WherePredicate, parse_macro_input, parse_quote,
};
#[proc_macro_derive(Refineable, attributes(refineable))]
pub fn derive_refineable(input: TokenStream) -> TokenStream {
let DeriveInput {
ident,
data,
generics,
attrs,
..
} = parse_macro_input!(input);
let refineable_attr = attrs.iter().find(|attr| attr.path().is_ident("refineable"));
let mut impl_debug_on_refinement = false;
let mut derives_serialize = false;
let mut refinement_traits_to_derive = vec![];
if let Some(refineable_attr) = refineable_attr {
let _ = refineable_attr.parse_nested_meta(|meta| {
if meta.path.is_ident("Debug") {
impl_debug_on_refinement = true;
} else {
if meta.path.is_ident("Serialize") {
derives_serialize = true;
}
refinement_traits_to_derive.push(meta.path);
}
Ok(())
});
}
let refinement_ident = format_ident!("{}Refinement", ident);
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let fields = match data {
syn::Data::Struct(syn::DataStruct {
fields: syn::Fields::Named(FieldsNamed { named, .. }),
..
}) => named.into_iter().collect::<Vec<Field>>(),
_ => panic!("This derive macro only supports structs with named fields"),
};
let field_names: Vec<_> = fields.iter().map(|f| f.ident.as_ref().unwrap()).collect();
let field_visibilities: Vec<_> = fields.iter().map(|f| &f.vis).collect();
let wrapped_types: Vec<_> = fields.iter().map(|f| get_wrapper_type(f, &f.ty)).collect();
let field_attributes: Vec<TokenStream2> = fields
.iter()
.map(|f| {
if derives_serialize {
if is_refineable_field(f) {
quote! { #[serde(default, skip_serializing_if = "::refineable::IsEmpty::is_empty")] }
} else {
quote! { #[serde(skip_serializing_if = "::std::option::Option::is_none")] }
}
} else {
quote! {}
}
})
.collect();
// Create trait bound that each wrapped type must implement Clone
let type_param_bounds: Vec<_> = wrapped_types
.iter()
.map(|ty| {
WherePredicate::Type(PredicateType {
lifetimes: None,
bounded_ty: ty.clone(),
colon_token: Default::default(),
bounds: {
let mut punctuated = syn::punctuated::Punctuated::new();
punctuated.push_value(TypeParamBound::Trait(TraitBound {
paren_token: None,
modifier: syn::TraitBoundModifier::None,
lifetimes: None,
path: parse_quote!(Clone),
}));
punctuated
},
})
})
.collect();
// Append to where_clause or create a new one if it doesn't exist
let where_clause = match where_clause.cloned() {
Some(mut where_clause) => {
where_clause.predicates.extend(type_param_bounds);
where_clause.clone()
}
None => WhereClause {
where_token: Default::default(),
predicates: type_param_bounds.into_iter().collect(),
},
};
let refineable_refine_assignments: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
let is_optional = is_optional_field(field);
if is_refineable {
quote! {
self.#name.refine(&refinement.#name);
}
} else if is_optional {
quote! {
if let Some(value) = &refinement.#name {
self.#name = Some(value.clone());
}
}
} else {
quote! {
if let Some(value) = &refinement.#name {
self.#name = value.clone();
}
}
}
})
.collect();
let refineable_refined_assignments: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
let is_optional = is_optional_field(field);
if is_refineable {
quote! {
self.#name = self.#name.refined(refinement.#name);
}
} else if is_optional {
quote! {
if let Some(value) = refinement.#name {
self.#name = Some(value);
}
}
} else {
quote! {
if let Some(value) = refinement.#name {
self.#name = value;
}
}
}
})
.collect();
let refinement_refine_assignments: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
if is_refineable {
quote! {
self.#name.refine(&refinement.#name);
}
} else {
quote! {
if let Some(value) = &refinement.#name {
self.#name = Some(value.clone());
}
}
}
})
.collect();
let refinement_refined_assignments: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
if is_refineable {
quote! {
self.#name = self.#name.refined(refinement.#name);
}
} else {
quote! {
if let Some(value) = refinement.#name {
self.#name = Some(value);
}
}
}
})
.collect();
let from_refinement_assignments: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
let is_optional = is_optional_field(field);
if is_refineable {
quote! {
#name: value.#name.into(),
}
} else if is_optional {
quote! {
#name: value.#name.map(|v| v.into()),
}
} else {
quote! {
#name: value.#name.map(|v| v.into()).unwrap_or_default(),
}
}
})
.collect();
let debug_impl = if impl_debug_on_refinement {
let refinement_field_debugs: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
quote! {
if self.#name.is_some() {
debug_struct.field(stringify!(#name), &self.#name);
} else {
all_some = false;
}
}
})
.collect();
quote! {
impl #impl_generics std::fmt::Debug for #refinement_ident #ty_generics
#where_clause
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut debug_struct = f.debug_struct(stringify!(#refinement_ident));
let mut all_some = true;
#( #refinement_field_debugs )*
if all_some {
debug_struct.finish()
} else {
debug_struct.finish_non_exhaustive()
}
}
}
}
} else {
quote! {}
};
let refinement_is_empty_conditions: Vec<TokenStream2> = fields
.iter()
.enumerate()
.map(|(i, field)| {
let name = &field.ident;
let condition = if is_refineable_field(field) {
quote! { self.#name.is_empty() }
} else {
quote! { self.#name.is_none() }
};
if i < fields.len() - 1 {
quote! { #condition && }
} else {
condition
}
})
.collect();
let refineable_is_superset_conditions: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
let is_optional = is_optional_field(field);
if is_refineable {
quote! {
if !self.#name.is_superset_of(&refinement.#name) {
return false;
}
}
} else if is_optional {
quote! {
if refinement.#name.is_some() && &self.#name != &refinement.#name {
return false;
}
}
} else {
quote! {
if let Some(refinement_value) = &refinement.#name {
if &self.#name != refinement_value {
return false;
}
}
}
}
})
.collect();
let refinement_is_superset_conditions: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
if is_refineable {
quote! {
if !self.#name.is_superset_of(&refinement.#name) {
return false;
}
}
} else {
quote! {
if refinement.#name.is_some() && &self.#name != &refinement.#name {
return false;
}
}
}
})
.collect();
let refineable_subtract_assignments: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
let is_optional = is_optional_field(field);
if is_refineable {
quote! {
#name: self.#name.subtract(&refinement.#name),
}
} else if is_optional {
quote! {
#name: if &self.#name == &refinement.#name {
None
} else {
self.#name.clone()
},
}
} else {
quote! {
#name: if let Some(refinement_value) = &refinement.#name {
if &self.#name == refinement_value {
None
} else {
Some(self.#name.clone())
}
} else {
Some(self.#name.clone())
},
}
}
})
.collect();
let refinement_subtract_assignments: Vec<TokenStream2> = fields
.iter()
.map(|field| {
let name = &field.ident;
let is_refineable = is_refineable_field(field);
if is_refineable {
quote! {
#name: self.#name.subtract(&refinement.#name),
}
} else {
quote! {
#name: if &self.#name == &refinement.#name {
None
} else {
self.#name.clone()
},
}
}
})
.collect();
let mut derive_stream = quote! {};
for trait_to_derive in refinement_traits_to_derive {
derive_stream.extend(quote! { #[derive(#trait_to_derive)] })
}
let r#gen = quote! {
/// A refinable version of [`#ident`], see that documentation for details.
#[derive(Clone)]
#derive_stream
pub struct #refinement_ident #impl_generics {
#(
#[allow(missing_docs)]
#field_attributes
#field_visibilities #field_names: #wrapped_types
),*
}
impl #impl_generics Refineable for #ident #ty_generics
#where_clause
{
type Refinement = #refinement_ident #ty_generics;
fn refine(&mut self, refinement: &Self::Refinement) {
#( #refineable_refine_assignments )*
}
fn refined(mut self, refinement: Self::Refinement) -> Self {
#( #refineable_refined_assignments )*
self
}
fn is_superset_of(&self, refinement: &Self::Refinement) -> bool
{
#( #refineable_is_superset_conditions )*
true
}
fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement
{
#refinement_ident {
#( #refineable_subtract_assignments )*
}
}
}
impl #impl_generics Refineable for #refinement_ident #ty_generics
#where_clause
{
type Refinement = #refinement_ident #ty_generics;
fn refine(&mut self, refinement: &Self::Refinement) {
#( #refinement_refine_assignments )*
}
fn refined(mut self, refinement: Self::Refinement) -> Self {
#( #refinement_refined_assignments )*
self
}
fn is_superset_of(&self, refinement: &Self::Refinement) -> bool
{
#( #refinement_is_superset_conditions )*
true
}
fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement
{
#refinement_ident {
#( #refinement_subtract_assignments )*
}
}
}
impl #impl_generics ::refineable::IsEmpty for #refinement_ident #ty_generics
#where_clause
{
fn is_empty(&self) -> bool {
#( #refinement_is_empty_conditions )*
}
}
impl #impl_generics From<#refinement_ident #ty_generics> for #ident #ty_generics
#where_clause
{
fn from(value: #refinement_ident #ty_generics) -> Self {
Self {
#( #from_refinement_assignments )*
}
}
}
impl #impl_generics ::core::default::Default for #refinement_ident #ty_generics
#where_clause
{
fn default() -> Self {
#refinement_ident {
#( #field_names: Default::default() ),*
}
}
}
impl #impl_generics #refinement_ident #ty_generics
#where_clause
{
/// Returns `true` if all fields are `Some`
pub fn is_some(&self) -> bool {
#(
if self.#field_names.is_some() {
return true;
}
)*
false
}
}
#debug_impl
};
r#gen.into()
}
fn is_refineable_field(f: &Field) -> bool {
f.attrs
.iter()
.any(|attr| attr.path().is_ident("refineable"))
}
fn is_optional_field(f: &Field) -> bool {
if let Type::Path(typepath) = &f.ty
&& typepath.qself.is_none()
{
let segments = &typepath.path.segments;
if segments.len() == 1 && segments.iter().any(|s| s.ident == "Option") {
return true;
}
}
false
}
fn get_wrapper_type(field: &Field, ty: &Type) -> syn::Type {
if is_refineable_field(field) {
let struct_name = if let Type::Path(tp) = ty {
tp.path.segments.last().unwrap().ident.clone()
} else {
panic!("Expected struct type for a refineable field");
};
let refinement_struct_name = format_ident!("{}Refinement", struct_name);
let generics = if let Type::Path(tp) = ty {
&tp.path.segments.last().unwrap().arguments
} else {
&syn::PathArguments::None
};
parse_quote!(#refinement_struct_name #generics)
} else if is_optional_field(field) {
ty.clone()
} else {
parse_quote!(Option<#ty>)
}
}
-132
View File
@@ -1,132 +0,0 @@
pub use derive_refineable::Refineable;
/// A trait for types that can be refined with partial updates.
///
/// The `Refineable` trait enables hierarchical configuration patterns where a base configuration
/// can be selectively overridden by refinements. This is particularly useful for styling and
/// settings, and theme hierarchies.
///
/// # Derive Macro
///
/// The `#[derive(Refineable)]` macro automatically generates a companion refinement type and
/// implements this trait. For a struct `Style`, it creates `StyleRefinement` where each field is
/// wrapped appropriately:
///
/// - **Refineable fields** (marked with `#[refineable]`): Become the corresponding refinement type
/// (e.g., `Bar` becomes `BarRefinement`)
/// - **Optional fields** (`Option<T>`): Remain as `Option<T>`
/// - **Regular fields**: Become `Option<T>`
///
/// ## Attributes
///
/// The derive macro supports these attributes on the struct:
/// - `#[refineable(Debug)]`: Implements `Debug` for the refinement type
/// - `#[refineable(Serialize)]`: Derives `Serialize` which skips serializing `None`
/// - `#[refineable(OtherTrait)]`: Derives additional traits on the refinement type
///
/// Fields can be marked with:
/// - `#[refineable]`: Field is itself refineable (uses nested refinement type)
pub trait Refineable: Clone {
type Refinement: Refineable<Refinement = Self::Refinement> + IsEmpty + Default;
/// Applies the given refinement to this instance, modifying it in place.
///
/// Only non-empty values in the refinement are applied.
///
/// * For refineable fields, this recursively calls `refine`.
/// * For other fields, the value is replaced if present in the refinement.
fn refine(&mut self, refinement: &Self::Refinement);
/// Returns a new instance with the refinement applied, equivalent to cloning `self` and calling
/// `refine` on it.
fn refined(self, refinement: Self::Refinement) -> Self;
/// Creates an instance from a cascade by merging all refinements atop the default value.
fn from_cascade(cascade: &Cascade<Self>) -> Self
where
Self: Default + Sized,
{
Self::default().refined(cascade.merged())
}
/// Returns `true` if this instance would contain all values from the refinement.
///
/// For refineable fields, this recursively checks `is_superset_of`. For other fields, this
/// checks if the refinement's `Some` values match this instance's values.
fn is_superset_of(&self, refinement: &Self::Refinement) -> bool;
/// Returns a refinement that represents the difference between this instance and the given
/// refinement.
///
/// For refineable fields, this recursively calls `subtract`. For other fields, the field is
/// `None` if the field's value is equal to the refinement.
fn subtract(&self, refinement: &Self::Refinement) -> Self::Refinement;
}
pub trait IsEmpty {
/// Returns `true` if applying this refinement would have no effect.
fn is_empty(&self) -> bool;
}
/// A cascade of refinements that can be merged in priority order.
///
/// A cascade maintains a sequence of optional refinements where later entries
/// take precedence over earlier ones. The first slot (index 0) is always the
/// base refinement and is guaranteed to be present.
///
/// This is useful for implementing configuration hierarchies like CSS cascading,
/// where styles from different sources (user agent, user, author) are combined
/// with specific precedence rules.
pub struct Cascade<S: Refineable>(Vec<Option<S::Refinement>>);
impl<S: Refineable + Default> Default for Cascade<S> {
fn default() -> Self {
Self(vec![Some(Default::default())])
}
}
/// A handle to a specific slot in a cascade.
///
/// Slots are used to identify specific positions in the cascade where
/// refinements can be set or updated.
#[derive(Copy, Clone)]
pub struct CascadeSlot(usize);
impl<S: Refineable + Default> Cascade<S> {
/// Reserves a new slot in the cascade and returns a handle to it.
///
/// The new slot is initially empty (`None`) and can be populated later
/// using `set()`.
pub fn reserve(&mut self) -> CascadeSlot {
self.0.push(None);
CascadeSlot(self.0.len() - 1)
}
/// Returns a mutable reference to the base refinement (slot 0).
///
/// The base refinement is always present and serves as the foundation
/// for the cascade.
pub fn base(&mut self) -> &mut S::Refinement {
self.0[0].as_mut().unwrap()
}
/// Sets the refinement for a specific slot in the cascade.
///
/// Setting a slot to `None` effectively removes it from consideration
/// during merging.
pub fn set(&mut self, slot: CascadeSlot, refinement: Option<S::Refinement>) {
self.0[slot.0] = refinement
}
/// Merges all refinements in the cascade into a single refinement.
///
/// Refinements are applied in order, with later slots taking precedence.
/// Empty slots (`None`) are skipped during merging.
pub fn merged(&self) -> S::Refinement {
let mut merged = self.0[0].clone().unwrap();
for refinement in self.0.iter().skip(1).flatten() {
merged.refine(refinement);
}
merged
}
}
-31
View File
@@ -1,31 +0,0 @@
[package]
name = "reqwest_client"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[features]
test-support = []
[lib]
path = "src/reqwest_client.rs"
doctest = true
[dependencies]
anyhow.workspace = true
bytes.workspace = true
futures.workspace = true
http_client.workspace = true
http_client_tls.workspace = true
serde.workspace = true
log.workspace = true
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
regex.workspace = true
reqwest.workspace = true
[dev-dependencies]
gpui.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-320
View File
@@ -1,320 +0,0 @@
use std::error::Error;
use std::sync::{LazyLock, OnceLock};
use std::{borrow::Cow, mem, pin::Pin, task::Poll, time::Duration};
use anyhow::anyhow;
use bytes::{BufMut, Bytes, BytesMut};
use futures::{AsyncRead, FutureExt as _, TryStreamExt as _};
use http_client::{RedirectPolicy, Url, http};
use regex::Regex;
use reqwest::{
header::{HeaderMap, HeaderValue},
redirect,
};
const DEFAULT_CAPACITY: usize = 4096;
static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
static REDACT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"key=[^&]+").unwrap());
pub struct ReqwestClient {
client: reqwest::Client,
proxy: Option<Url>,
user_agent: Option<HeaderValue>,
handle: tokio::runtime::Handle,
}
impl ReqwestClient {
fn builder() -> reqwest::ClientBuilder {
reqwest::Client::builder()
.use_rustls_tls()
.connect_timeout(Duration::from_secs(10))
}
pub fn new() -> Self {
Self::builder()
.build()
.expect("Failed to initialize HTTP client")
.into()
}
pub fn user_agent(agent: &str) -> anyhow::Result<Self> {
let mut map = HeaderMap::new();
map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?);
let client = Self::builder().default_headers(map).build()?;
Ok(client.into())
}
pub fn proxy_and_user_agent(proxy: Option<Url>, user_agent: &str) -> anyhow::Result<Self> {
let user_agent = HeaderValue::from_str(user_agent)?;
let mut map = HeaderMap::new();
map.insert(http::header::USER_AGENT, user_agent.clone());
let mut client = Self::builder().default_headers(map);
let client_has_proxy;
if let Some(proxy) = proxy.as_ref().and_then(|proxy_url| {
reqwest::Proxy::all(proxy_url.clone())
.inspect_err(|e| {
log::error!(
"Failed to parse proxy URL '{}': {}",
proxy_url,
e.source().unwrap_or(&e as &_)
)
})
.ok()
}) {
// Respect NO_PROXY env var
client = client.proxy(proxy.no_proxy(reqwest::NoProxy::from_env()));
client_has_proxy = true;
} else {
client_has_proxy = false;
};
let client = client
.use_preconfigured_tls(http_client_tls::tls_config())
.build()?;
let mut client: ReqwestClient = client.into();
client.proxy = client_has_proxy.then_some(proxy).flatten();
client.user_agent = Some(user_agent);
Ok(client)
}
}
pub fn runtime() -> &'static tokio::runtime::Runtime {
RUNTIME.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
// Since we now have two executors, let's try to keep our footprint small
.worker_threads(1)
.enable_all()
.build()
.expect("Failed to initialize HTTP client")
})
}
impl From<reqwest::Client> for ReqwestClient {
fn from(client: reqwest::Client) -> Self {
let handle = tokio::runtime::Handle::try_current().unwrap_or_else(|_| {
log::debug!("no tokio runtime found, creating one for Reqwest...");
runtime().handle().clone()
});
Self {
client,
handle,
proxy: None,
user_agent: None,
}
}
}
// This struct is essentially a re-implementation of
// https://docs.rs/tokio-util/0.7.12/tokio_util/io/struct.ReaderStream.html
// except outside of Tokio's aegis
struct StreamReader {
reader: Option<Pin<Box<dyn futures::AsyncRead + Send + Sync>>>,
buf: BytesMut,
capacity: usize,
}
impl StreamReader {
fn new(reader: Pin<Box<dyn futures::AsyncRead + Send + Sync>>) -> Self {
Self {
reader: Some(reader),
buf: BytesMut::new(),
capacity: DEFAULT_CAPACITY,
}
}
}
impl futures::Stream for StreamReader {
type Item = std::io::Result<Bytes>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut();
let mut reader = match this.reader.take() {
Some(r) => r,
None => return Poll::Ready(None),
};
if this.buf.capacity() == 0 {
let capacity = this.capacity;
this.buf.reserve(capacity);
}
match poll_read_buf(&mut reader, cx, &mut this.buf) {
Poll::Pending => Poll::Pending,
Poll::Ready(Err(err)) => {
self.reader = None;
Poll::Ready(Some(Err(err)))
}
Poll::Ready(Ok(0)) => {
self.reader = None;
Poll::Ready(None)
}
Poll::Ready(Ok(_)) => {
let chunk = this.buf.split();
self.reader = Some(reader);
Poll::Ready(Some(Ok(chunk.freeze())))
}
}
}
}
/// Implementation from <https://docs.rs/tokio-util/0.7.12/src/tokio_util/util/poll_buf.rs.html>
/// Specialized for this use case
pub fn poll_read_buf(
io: &mut Pin<Box<dyn futures::AsyncRead + Send + Sync>>,
cx: &mut std::task::Context<'_>,
buf: &mut BytesMut,
) -> Poll<std::io::Result<usize>> {
if !buf.has_remaining_mut() {
return Poll::Ready(Ok(0));
}
let n = {
let dst = buf.chunk_mut();
// Safety: `chunk_mut()` returns a `&mut UninitSlice`, and `UninitSlice` is a
// transparent wrapper around `[MaybeUninit<u8>]`.
let dst = unsafe { &mut *(dst as *mut _ as *mut [std::mem::MaybeUninit<u8>]) };
let mut buf = tokio::io::ReadBuf::uninit(dst);
let ptr = buf.filled().as_ptr();
let unfilled_portion = buf.initialize_unfilled();
// SAFETY: Pin projection
let io_pin = unsafe { Pin::new_unchecked(io) };
std::task::ready!(io_pin.poll_read(cx, unfilled_portion)?);
// Ensure the pointer does not change from under us
assert_eq!(ptr, buf.filled().as_ptr());
buf.filled().len()
};
// Safety: This is guaranteed to be the number of initialized (and read)
// bytes due to the invariants provided by `ReadBuf::filled`.
unsafe {
buf.advance_mut(n);
}
Poll::Ready(Ok(n))
}
fn redact_error(mut error: reqwest::Error) -> reqwest::Error {
if let Some(url) = error.url_mut()
&& let Some(query) = url.query()
&& let Cow::Owned(redacted) = REDACT_REGEX.replace_all(query, "key=REDACTED")
{
url.set_query(Some(redacted.as_str()));
}
error
}
impl http_client::HttpClient for ReqwestClient {
fn proxy(&self) -> Option<&Url> {
self.proxy.as_ref()
}
fn user_agent(&self) -> Option<&HeaderValue> {
self.user_agent.as_ref()
}
fn send(
&self,
req: http::Request<http_client::AsyncBody>,
) -> futures::future::BoxFuture<
'static,
anyhow::Result<http_client::Response<http_client::AsyncBody>>,
> {
let (parts, body) = req.into_parts();
let mut request = self.client.request(parts.method, parts.uri.to_string());
request = request.headers(parts.headers);
if let Some(redirect_policy) = parts.extensions.get::<RedirectPolicy>() {
request = request.redirect_policy(match redirect_policy {
RedirectPolicy::NoFollow => redirect::Policy::none(),
RedirectPolicy::FollowLimit(limit) => redirect::Policy::limited(*limit as usize),
RedirectPolicy::FollowAll => redirect::Policy::limited(100),
});
}
let request = request.body(match body.0 {
http_client::Inner::Empty => reqwest::Body::default(),
http_client::Inner::Bytes(cursor) => cursor.into_inner().into(),
http_client::Inner::AsyncReader(stream) => {
reqwest::Body::wrap_stream(StreamReader::new(stream))
}
});
let handle = self.handle.clone();
async move {
let mut response = handle
.spawn(async { request.send().await })
.await?
.map_err(redact_error)?;
let headers = mem::take(response.headers_mut());
let mut builder = http::Response::builder()
.status(response.status().as_u16())
.version(response.version());
*builder.headers_mut().unwrap() = headers;
let bytes = response
.bytes_stream()
.map_err(futures::io::Error::other)
.into_async_read();
let body = http_client::AsyncBody::from_reader(bytes);
builder.body(body).map_err(|e| anyhow!(e))
}
.boxed()
}
}
#[cfg(test)]
mod tests {
use http_client::{HttpClient, Url};
use crate::ReqwestClient;
#[test]
fn test_proxy_uri() {
let client = ReqwestClient::new();
assert_eq!(client.proxy(), None);
let proxy = Url::parse("http://localhost:10809").unwrap();
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = Url::parse("https://localhost:10809").unwrap();
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = Url::parse("socks4://localhost:10808").unwrap();
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = Url::parse("socks4a://localhost:10808").unwrap();
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = Url::parse("socks5://localhost:10808").unwrap();
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
let proxy = Url::parse("socks5h://localhost:10808").unwrap();
let client = ReqwestClient::proxy_and_user_agent(Some(proxy.clone()), "test").unwrap();
assert_eq!(client.proxy(), Some(&proxy));
}
#[test]
fn test_invalid_proxy_uri() {
let proxy = Url::parse("socks://127.0.0.1:20170").unwrap();
let client = ReqwestClient::proxy_and_user_agent(Some(proxy), "test").unwrap();
assert!(
client.proxy.is_none(),
"An invalid proxy URL should add no proxy to the client!"
)
}
}
-24
View File
@@ -1,24 +0,0 @@
[package]
name = "scheduler"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/scheduler.rs"
doctest = false
[features]
test-support = []
[dependencies]
async-task.workspace = true
backtrace.workspace = true
chrono.workspace = true
futures.workspace = true
parking_lot.workspace = true
rand.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-47
View File
@@ -1,47 +0,0 @@
use chrono::{DateTime, Utc};
use parking_lot::Mutex;
use std::time::{Duration, Instant};
pub trait Clock {
fn utc_now(&self) -> DateTime<Utc>;
fn now(&self) -> Instant;
}
pub struct TestClock(Mutex<TestClockState>);
struct TestClockState {
now: Instant,
utc_now: DateTime<Utc>,
}
impl TestClock {
pub fn new() -> Self {
const START_TIME: &str = "2025-07-01T23:59:58-00:00";
let utc_now = DateTime::parse_from_rfc3339(START_TIME).unwrap().to_utc();
Self(Mutex::new(TestClockState {
now: Instant::now(),
utc_now,
}))
}
pub fn set_utc_now(&self, now: DateTime<Utc>) {
let mut state = self.0.lock();
state.utc_now = now;
}
pub fn advance(&self, duration: Duration) {
let mut state = self.0.lock();
state.now += duration;
state.utc_now += duration;
}
}
impl Clock for TestClock {
fn utc_now(&self) -> DateTime<Utc> {
self.0.lock().utc_now
}
fn now(&self) -> Instant {
self.0.lock().now
}
}
-223
View File
@@ -1,223 +0,0 @@
use crate::{Scheduler, SessionId, Timer};
use futures::FutureExt as _;
use std::{
future::Future,
marker::PhantomData,
mem::ManuallyDrop,
panic::Location,
pin::Pin,
rc::Rc,
sync::Arc,
task::{Context, Poll},
thread::{self, ThreadId},
time::Duration,
};
#[derive(Clone)]
pub struct ForegroundExecutor {
session_id: SessionId,
scheduler: Arc<dyn Scheduler>,
not_send: PhantomData<Rc<()>>,
}
impl ForegroundExecutor {
pub fn new(session_id: SessionId, scheduler: Arc<dyn Scheduler>) -> Self {
Self {
session_id,
scheduler,
not_send: PhantomData,
}
}
#[track_caller]
pub fn spawn<F>(&self, future: F) -> Task<F::Output>
where
F: Future + 'static,
F::Output: 'static,
{
let session_id = self.session_id;
let scheduler = Arc::clone(&self.scheduler);
let (runnable, task) = spawn_local_with_source_location(future, move |runnable| {
scheduler.schedule_foreground(session_id, runnable);
});
runnable.schedule();
Task(TaskState::Spawned(task))
}
pub fn block_on<Fut: Future>(&self, future: Fut) -> Fut::Output {
let mut output = None;
self.scheduler.block(
Some(self.session_id),
async { output = Some(future.await) }.boxed_local(),
None,
);
output.unwrap()
}
pub fn block_with_timeout<Fut: Unpin + Future>(
&self,
timeout: Duration,
mut future: Fut,
) -> Result<Fut::Output, Fut> {
let mut output = None;
self.scheduler.block(
Some(self.session_id),
async { output = Some((&mut future).await) }.boxed_local(),
Some(timeout),
);
output.ok_or(future)
}
pub fn timer(&self, duration: Duration) -> Timer {
self.scheduler.timer(duration)
}
}
#[derive(Clone)]
pub struct BackgroundExecutor {
scheduler: Arc<dyn Scheduler>,
}
impl BackgroundExecutor {
pub fn new(scheduler: Arc<dyn Scheduler>) -> Self {
Self { scheduler }
}
pub fn spawn<F>(&self, future: F) -> Task<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let scheduler = Arc::clone(&self.scheduler);
let (runnable, task) = async_task::spawn(future, move |runnable| {
scheduler.schedule_background(runnable);
});
runnable.schedule();
Task(TaskState::Spawned(task))
}
pub fn timer(&self, duration: Duration) -> Timer {
self.scheduler.timer(duration)
}
pub fn scheduler(&self) -> &Arc<dyn Scheduler> {
&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<T>(TaskState<T>);
#[derive(Debug)]
enum TaskState<T> {
/// A task that is ready to return a value
Ready(Option<T>),
/// A task that is currently running.
Spawned(async_task::Task<T>),
}
impl<T> Task<T> {
/// Creates a new task that will resolve with the value
pub fn ready(val: T) -> Self {
Task(TaskState::Ready(Some(val)))
}
pub fn is_ready(&self) -> bool {
match &self.0 {
TaskState::Ready(_) => true,
TaskState::Spawned(task) => task.is_finished(),
}
}
/// Detaching a task runs it to completion in the background
pub fn detach(self) {
match self {
Task(TaskState::Ready(_)) => {}
Task(TaskState::Spawned(task)) => task.detach(),
}
}
}
impl<T> Future for Task<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
match unsafe { self.get_unchecked_mut() } {
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx),
}
}
}
/// Variant of `async_task::spawn_local` that includes the source location of the spawn in panics.
///
/// Copy-modified from:
/// <https://github.com/smol-rs/async-task/blob/ca9dbe1db9c422fd765847fa91306e30a6bb58a9/src/runnable.rs#L405>
#[track_caller]
fn spawn_local_with_source_location<Fut, S>(
future: Fut,
schedule: S,
) -> (async_task::Runnable, async_task::Task<Fut::Output, ()>)
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<F> {
id: ThreadId,
inner: ManuallyDrop<F>,
location: &'static Location<'static>,
}
impl<F> Drop for Checked<F> {
fn drop(&mut self) {
assert!(
self.id == thread_id(),
"local task dropped by a thread that didn't spawn it. Task spawned at {}",
self.location
);
unsafe {
ManuallyDrop::drop(&mut self.inner);
}
}
}
impl<F: Future> Future for Checked<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
assert!(
self.id == thread_id(),
"local task polled by a thread that didn't spawn it. Task spawned at {}",
self.location
);
unsafe { self.map_unchecked_mut(|c| &mut *c.inner).poll(cx) }
}
}
// Wrap the future into one that checks which thread it's on.
let future = Checked {
id: thread_id(),
inner: ManuallyDrop::new(future),
location: Location::caller(),
};
unsafe { async_task::spawn_unchecked(future, schedule) }
}
-63
View File
@@ -1,63 +0,0 @@
mod clock;
mod executor;
mod test_scheduler;
#[cfg(test)]
mod tests;
pub use clock::*;
pub use executor::*;
pub use test_scheduler::*;
use async_task::Runnable;
use futures::{FutureExt as _, channel::oneshot, future::LocalBoxFuture};
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
pub trait Scheduler: Send + Sync {
fn block(
&self,
session_id: Option<SessionId>,
future: LocalBoxFuture<()>,
timeout: Option<Duration>,
);
fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable);
fn schedule_background(&self, runnable: Runnable);
fn timer(&self, timeout: Duration) -> Timer;
fn clock(&self) -> Arc<dyn Clock>;
fn as_test(&self) -> &TestScheduler {
panic!("this is not a test scheduler")
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct SessionId(u16);
impl SessionId {
pub fn new(id: u16) -> Self {
SessionId(id)
}
}
pub struct Timer(oneshot::Receiver<()>);
impl Timer {
pub fn new(rx: oneshot::Receiver<()>) -> Self {
Timer(rx)
}
}
impl Future for Timer {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
match self.0.poll_unpin(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
}
}
}
-541
View File
@@ -1,541 +0,0 @@
use crate::{
BackgroundExecutor, Clock, ForegroundExecutor, Scheduler, SessionId, TestClock, Timer,
};
use async_task::Runnable;
use backtrace::{Backtrace, BacktraceFrame};
use futures::{FutureExt as _, channel::oneshot, future::LocalBoxFuture};
use parking_lot::Mutex;
use rand::prelude::*;
use std::{
any::type_name_of_val,
collections::{BTreeMap, VecDeque},
env,
fmt::Write,
future::Future,
mem,
ops::RangeInclusive,
panic::{self, AssertUnwindSafe},
pin::Pin,
sync::{
Arc,
atomic::{AtomicBool, Ordering::SeqCst},
},
task::{Context, Poll, RawWaker, RawWakerVTable, Waker},
thread::{self, Thread},
time::{Duration, Instant},
};
const PENDING_TRACES_VAR_NAME: &str = "PENDING_TRACES";
pub struct TestScheduler {
clock: Arc<TestClock>,
rng: Arc<Mutex<StdRng>>,
state: Arc<Mutex<SchedulerState>>,
thread: Thread,
}
impl TestScheduler {
/// Run a test once with default configuration (seed 0)
pub fn once<R>(f: impl AsyncFnOnce(Arc<TestScheduler>) -> R) -> R {
Self::with_seed(0, f)
}
/// Run a test multiple times with sequential seeds (0, 1, 2, ...)
pub fn many<R>(
default_iterations: usize,
mut f: impl AsyncFnMut(Arc<TestScheduler>) -> R,
) -> Vec<R> {
let num_iterations = std::env::var("ITERATIONS")
.map(|iterations| iterations.parse().unwrap())
.unwrap_or(default_iterations);
let seed = std::env::var("SEED")
.map(|seed| seed.parse().unwrap())
.unwrap_or(0);
(seed..num_iterations as u64)
.map(|seed| {
let mut unwind_safe_f = AssertUnwindSafe(&mut f);
eprintln!("Running seed: {seed}");
match panic::catch_unwind(move || Self::with_seed(seed, &mut *unwind_safe_f)) {
Ok(result) => result,
Err(error) => {
eprintln!("\x1b[31mFailing Seed: {seed}\x1b[0m");
panic::resume_unwind(error);
}
}
})
.collect()
}
fn with_seed<R>(seed: u64, f: impl AsyncFnOnce(Arc<TestScheduler>) -> R) -> R {
let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::with_seed(seed)));
let future = f(scheduler.clone());
let result = scheduler.foreground().block_on(future);
scheduler.run(); // Ensure spawned tasks finish up before returning in tests
result
}
pub fn new(config: TestSchedulerConfig) -> Self {
Self {
rng: Arc::new(Mutex::new(StdRng::seed_from_u64(config.seed))),
state: Arc::new(Mutex::new(SchedulerState {
runnables: VecDeque::new(),
timers: Vec::new(),
blocked_sessions: Vec::new(),
randomize_order: config.randomize_order,
allow_parking: config.allow_parking,
timeout_ticks: config.timeout_ticks,
next_session_id: SessionId(0),
capture_pending_traces: config.capture_pending_traces,
pending_traces: BTreeMap::new(),
next_trace_id: TraceId(0),
})),
clock: Arc::new(TestClock::new()),
thread: thread::current(),
}
}
pub fn clock(&self) -> Arc<TestClock> {
self.clock.clone()
}
pub fn rng(&self) -> Arc<Mutex<StdRng>> {
self.rng.clone()
}
pub fn set_timeout_ticks(&self, timeout_ticks: RangeInclusive<usize>) {
self.state.lock().timeout_ticks = timeout_ticks;
}
pub fn allow_parking(&self) {
self.state.lock().allow_parking = true;
}
pub fn forbid_parking(&self) {
self.state.lock().allow_parking = false;
}
/// Create a foreground executor for this scheduler
pub fn foreground(self: &Arc<Self>) -> ForegroundExecutor {
let session_id = {
let mut state = self.state.lock();
state.next_session_id.0 += 1;
state.next_session_id
};
ForegroundExecutor::new(session_id, self.clone())
}
/// Create a background executor for this scheduler
pub fn background(self: &Arc<Self>) -> BackgroundExecutor {
BackgroundExecutor::new(self.clone())
}
pub fn yield_random(&self) -> Yield {
let rng = &mut *self.rng.lock();
if rng.random_bool(0.1) {
Yield(rng.random_range(10..20))
} else {
Yield(rng.random_range(0..2))
}
}
pub fn run(&self) {
while self.step() {
// Continue until no work remains
}
}
pub fn run_with_clock_advancement(&self) {
while self.step() || self.advance_clock_to_next_timer() {
// Continue until no work remains
}
}
fn step(&self) -> bool {
let elapsed_timers = {
let mut state = self.state.lock();
let end_ix = state
.timers
.partition_point(|timer| timer.expiration <= self.clock.now());
state.timers.drain(..end_ix).collect::<Vec<_>>()
};
if !elapsed_timers.is_empty() {
return true;
}
let runnable = {
let state = &mut *self.state.lock();
let ix = state.runnables.iter().position(|runnable| {
runnable
.session_id
.is_none_or(|session_id| !state.blocked_sessions.contains(&session_id))
});
ix.and_then(|ix| state.runnables.remove(ix))
};
if let Some(runnable) = runnable {
runnable.run();
return true;
}
false
}
fn advance_clock_to_next_timer(&self) -> bool {
if let Some(timer) = self.state.lock().timers.first() {
self.clock.advance(timer.expiration - self.clock.now());
true
} else {
false
}
}
pub fn advance_clock(&self, duration: Duration) {
let next_now = self.clock.now() + duration;
loop {
self.run();
if let Some(timer) = self.state.lock().timers.first()
&& timer.expiration <= next_now
{
self.clock.advance(timer.expiration - self.clock.now());
} else {
break;
}
}
self.clock.advance(next_now - self.clock.now());
}
fn park(&self, deadline: Option<Instant>) -> bool {
if self.state.lock().allow_parking {
if let Some(deadline) = deadline {
let now = Instant::now();
let timeout = deadline.saturating_duration_since(now);
thread::park_timeout(timeout);
now.elapsed() < timeout
} else {
thread::park();
true
}
} else if deadline.is_some() {
false
} else if self.state.lock().capture_pending_traces {
let mut pending_traces = String::new();
for (_, trace) in mem::take(&mut self.state.lock().pending_traces) {
writeln!(pending_traces, "{:?}", exclude_wakers_from_trace(trace)).unwrap();
}
panic!("Parking forbidden. Pending traces:\n{}", pending_traces);
} else {
panic!(
"Parking forbidden. Re-run with {PENDING_TRACES_VAR_NAME}=1 to show pending traces"
);
}
}
}
impl Scheduler for TestScheduler {
/// Block until the given future completes, with an optional timeout. If the
/// future is unable to make progress at any moment before the timeout and
/// no other tasks or timers remain, we panic unless parking is allowed. If
/// parking is allowed, we block up to the timeout or indefinitely if none
/// is provided. This is to allow testing a mix of deterministic and
/// non-deterministic async behavior, such as when interacting with I/O in
/// an otherwise deterministic test.
fn block(
&self,
session_id: Option<SessionId>,
mut future: LocalBoxFuture<()>,
timeout: Option<Duration>,
) {
if let Some(session_id) = session_id {
self.state.lock().blocked_sessions.push(session_id);
}
let deadline = timeout.map(|timeout| Instant::now() + timeout);
let awoken = Arc::new(AtomicBool::new(false));
let waker = Box::new(TracingWaker {
id: None,
awoken: awoken.clone(),
thread: self.thread.clone(),
state: self.state.clone(),
});
let waker = unsafe { Waker::new(Box::into_raw(waker) as *const (), &WAKER_VTABLE) };
let max_ticks = if timeout.is_some() {
self.rng
.lock()
.random_range(self.state.lock().timeout_ticks.clone())
} else {
usize::MAX
};
let mut cx = Context::from_waker(&waker);
for _ in 0..max_ticks {
let Poll::Pending = future.poll_unpin(&mut cx) else {
break;
};
let mut stepped = None;
while self.rng.lock().random() {
let stepped = stepped.get_or_insert(false);
if self.step() {
*stepped = true;
} else {
break;
}
}
let stepped = stepped.unwrap_or(true);
let awoken = awoken.swap(false, SeqCst);
if !stepped && !awoken && !self.advance_clock_to_next_timer() {
if !self.park(deadline) {
break;
}
}
}
if session_id.is_some() {
self.state.lock().blocked_sessions.pop();
}
}
fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable) {
let mut state = self.state.lock();
let ix = if state.randomize_order {
let start_ix = state
.runnables
.iter()
.rposition(|task| task.session_id == Some(session_id))
.map_or(0, |ix| ix + 1);
self.rng
.lock()
.random_range(start_ix..=state.runnables.len())
} else {
state.runnables.len()
};
state.runnables.insert(
ix,
ScheduledRunnable {
session_id: Some(session_id),
runnable,
},
);
drop(state);
self.thread.unpark();
}
fn schedule_background(&self, runnable: Runnable) {
let mut state = self.state.lock();
let ix = if state.randomize_order {
self.rng.lock().random_range(0..=state.runnables.len())
} else {
state.runnables.len()
};
state.runnables.insert(
ix,
ScheduledRunnable {
session_id: None,
runnable,
},
);
drop(state);
self.thread.unpark();
}
fn timer(&self, duration: Duration) -> Timer {
let (tx, rx) = oneshot::channel();
let state = &mut *self.state.lock();
state.timers.push(ScheduledTimer {
expiration: self.clock.now() + duration,
_notify: tx,
});
state.timers.sort_by_key(|timer| timer.expiration);
Timer(rx)
}
fn clock(&self) -> Arc<dyn Clock> {
self.clock.clone()
}
fn as_test(&self) -> &TestScheduler {
self
}
}
#[derive(Clone, Debug)]
pub struct TestSchedulerConfig {
pub seed: u64,
pub randomize_order: bool,
pub allow_parking: bool,
pub capture_pending_traces: bool,
pub timeout_ticks: RangeInclusive<usize>,
}
impl TestSchedulerConfig {
pub fn with_seed(seed: u64) -> Self {
Self {
seed,
..Default::default()
}
}
}
impl Default for TestSchedulerConfig {
fn default() -> Self {
Self {
seed: 0,
randomize_order: true,
allow_parking: false,
capture_pending_traces: env::var(PENDING_TRACES_VAR_NAME)
.map_or(false, |var| var == "1" || var == "true"),
timeout_ticks: 0..=1000,
}
}
}
struct ScheduledRunnable {
session_id: Option<SessionId>,
runnable: Runnable,
}
impl ScheduledRunnable {
fn run(self) {
self.runnable.run();
}
}
struct ScheduledTimer {
expiration: Instant,
_notify: oneshot::Sender<()>,
}
struct SchedulerState {
runnables: VecDeque<ScheduledRunnable>,
timers: Vec<ScheduledTimer>,
blocked_sessions: Vec<SessionId>,
randomize_order: bool,
allow_parking: bool,
timeout_ticks: RangeInclusive<usize>,
next_session_id: SessionId,
capture_pending_traces: bool,
next_trace_id: TraceId,
pending_traces: BTreeMap<TraceId, Backtrace>,
}
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<TraceId>,
awoken: Arc<AtomicBool>,
thread: Thread,
state: Arc<Mutex<SchedulerState>>,
}
impl Clone for TracingWaker {
fn clone(&self) -> Self {
let mut state = self.state.lock();
let id = if state.capture_pending_traces {
let id = state.next_trace_id;
state.next_trace_id.0 += 1;
state.pending_traces.insert(id, Backtrace::new_unresolved());
Some(id)
} else {
None
};
Self {
id,
awoken: self.awoken.clone(),
thread: self.thread.clone(),
state: self.state.clone(),
}
}
}
impl Drop for TracingWaker {
fn drop(&mut self) {
if let Some(id) = self.id {
self.state.lock().pending_traces.remove(&id);
}
}
}
impl TracingWaker {
fn wake(self) {
self.wake_by_ref();
}
fn wake_by_ref(&self) {
if let Some(id) = self.id {
self.state.lock().pending_traces.remove(&id);
}
self.awoken.store(true, SeqCst);
self.thread.unpark();
}
fn clone_raw(waker: *const ()) -> RawWaker {
let waker = waker as *const TracingWaker;
let waker = unsafe { &*waker };
RawWaker::new(
Box::into_raw(Box::new(waker.clone())) as *const (),
&WAKER_VTABLE,
)
}
fn wake_raw(waker: *const ()) {
let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) };
waker.wake();
}
fn wake_by_ref_raw(waker: *const ()) {
let waker = waker as *const TracingWaker;
let waker = unsafe { &*waker };
waker.wake_by_ref();
}
fn drop_raw(waker: *const ()) {
let waker = unsafe { Box::from_raw(waker as *mut TracingWaker) };
drop(waker);
}
}
pub struct Yield(usize);
impl Future for Yield {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
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<BacktraceFrame> = 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)
}
-372
View File
@@ -1,372 +0,0 @@
use super::*;
use futures::{
FutureExt,
channel::{mpsc, oneshot},
executor::block_on,
future,
sink::SinkExt,
stream::{FuturesUnordered, StreamExt},
};
use std::{
cell::RefCell,
collections::{BTreeSet, HashSet},
pin::Pin,
rc::Rc,
sync::Arc,
task::{Context, Poll},
};
#[test]
fn test_foreground_executor_spawn() {
let result = TestScheduler::once(async |scheduler| {
let task = scheduler.foreground().spawn(async move { 42 });
task.await
});
assert_eq!(result, 42);
}
#[test]
fn test_background_executor_spawn() {
TestScheduler::once(async |scheduler| {
let task = scheduler.background().spawn(async move { 42 });
let result = task.await;
assert_eq!(result, 42);
});
}
#[test]
fn test_foreground_ordering() {
let mut traces = HashSet::new();
TestScheduler::many(100, async |scheduler| {
#[derive(Hash, PartialEq, Eq)]
struct TraceEntry {
session: usize,
task: usize,
}
let trace = Rc::new(RefCell::new(Vec::new()));
let foreground_1 = scheduler.foreground();
for task in 0..10 {
foreground_1
.spawn({
let trace = trace.clone();
async move {
trace.borrow_mut().push(TraceEntry { session: 0, task });
}
})
.detach();
}
let foreground_2 = scheduler.foreground();
for task in 0..10 {
foreground_2
.spawn({
let trace = trace.clone();
async move {
trace.borrow_mut().push(TraceEntry { session: 1, task });
}
})
.detach();
}
scheduler.run();
assert_eq!(
trace
.borrow()
.iter()
.filter(|entry| entry.session == 0)
.map(|entry| entry.task)
.collect::<Vec<_>>(),
(0..10).collect::<Vec<_>>()
);
assert_eq!(
trace
.borrow()
.iter()
.filter(|entry| entry.session == 1)
.map(|entry| entry.task)
.collect::<Vec<_>>(),
(0..10).collect::<Vec<_>>()
);
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::<Vec<_>>().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::<i32>();
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<String> {
let scheduler = Arc::new(TestScheduler::new(config));
let foreground = scheduler.foreground();
let background = scheduler.background();
let (sender, receiver) = mpsc::unbounded::<String>();
// Spawn foreground tasks
for i in 0..3 {
let mut sender = sender.clone();
foreground
.spawn(async move {
sender.send(format!("fg-{}", i)).await.ok();
})
.detach();
}
// Spawn background tasks
for i in 0..3 {
let mut sender = sender.clone();
background
.spawn(async move {
sender.send(format!("bg-{}", i)).await.ok();
})
.detach();
}
drop(sender); // Close sender to signal no more messages
scheduler.run();
receiver.collect().await
}
#[test]
fn test_block() {
let scheduler = Arc::new(TestScheduler::new(TestSchedulerConfig::default()));
let (tx, rx) = oneshot::channel();
// Spawn background task to send value
let _ = scheduler
.background()
.spawn(async move {
tx.send(42).unwrap();
})
.detach();
// Block on receiving the value
let result = scheduler.foreground().block_on(async { rx.await.unwrap() });
assert_eq!(result, 42);
}
#[test]
#[should_panic(expected = "futures_channel::oneshot::Inner")]
fn test_parking_panics() {
let config = TestSchedulerConfig {
capture_pending_traces: true,
..Default::default()
};
let scheduler = Arc::new(TestScheduler::new(config));
scheduler.foreground().block_on(async {
let (_tx, rx) = oneshot::channel::<()>();
rx.await.unwrap(); // This will never complete
});
}
#[test]
fn test_block_with_parking() {
let config = TestSchedulerConfig {
allow_parking: true,
..Default::default()
};
let scheduler = Arc::new(TestScheduler::new(config));
let (tx, rx) = oneshot::channel();
// Spawn background task to send value
let _ = scheduler
.background()
.spawn(async move {
tx.send(42).unwrap();
})
.detach();
// Block on receiving the value (will park if needed)
let result = scheduler.foreground().block_on(async { rx.await.unwrap() });
assert_eq!(result, 42);
}
#[test]
fn test_helper_methods() {
// Test the once method
let result = TestScheduler::once(async |scheduler: Arc<TestScheduler>| {
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<TestScheduler>| {
let background = scheduler.background();
background.spawn(async { 10 }).await
});
assert_eq!(results, vec![10, 10, 10]);
}
#[test]
fn test_block_with_timeout() {
// Test case: future completes within timeout
TestScheduler::once(async |scheduler| {
let foreground = scheduler.foreground();
let future = future::ready(42);
let output = foreground.block_with_timeout(Duration::from_millis(100), future);
assert_eq!(output.unwrap(), 42);
});
// Test case: future times out
TestScheduler::once(async |scheduler| {
let foreground = scheduler.foreground();
let future = future::pending::<()>();
let output = foreground.block_with_timeout(Duration::from_millis(50), future);
let _ = output.expect_err("future should not have finished");
});
// Test case: future makes progress via timer but still times out
let mut results = BTreeSet::new();
TestScheduler::many(100, async |scheduler| {
let task = scheduler.background().spawn(async move {
Yield { polls: 10 }.await;
42
});
let output = scheduler
.foreground()
.block_with_timeout(Duration::from_millis(50), task);
results.insert(output.ok());
});
assert_eq!(
results.into_iter().collect::<Vec<_>>(),
vec![None, Some(42)]
);
}
// When calling block, we shouldn't make progress on foreground-spawned futures with the same session id.
#[test]
fn test_block_does_not_progress_same_session_foreground() {
let mut task2_made_progress_once = false;
TestScheduler::many(1000, async |scheduler| {
let foreground1 = scheduler.foreground();
let foreground2 = scheduler.foreground();
let task1 = foreground1.spawn(async move {});
let task2 = foreground2.spawn(async move {});
foreground1.block_on(async {
scheduler.yield_random().await;
assert!(!task1.is_ready());
task2_made_progress_once |= task2.is_ready();
});
task1.await;
task2.await;
});
assert!(
task2_made_progress_once,
"Expected task from different foreground executor to make progress (at least once)"
);
}
struct Yield {
polls: usize,
}
impl Future for Yield {
type Output = ();
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.polls -= 1;
if self.polls == 0 {
Poll::Ready(())
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
-27
View File
@@ -1,27 +0,0 @@
[package]
name = "sum_tree"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "A sum tree data structure, a concurrency-friendly B-tree"
[lints]
workspace = true
[lib]
path = "src/sum_tree.rs"
doctest = false
[dependencies]
arrayvec = "0.7.1"
rayon.workspace = true
log.workspace = true
tracing.workspace = true
[dev-dependencies]
ctor.workspace = true
rand.workspace = true
[package.metadata.cargo-machete]
ignored = ["tracing"]
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-832
View File
@@ -1,832 +0,0 @@
use super::*;
use arrayvec::ArrayVec;
use std::{cmp::Ordering, mem, sync::Arc};
use tracing::instrument;
#[derive(Clone)]
struct StackEntry<'a, T: Item, D> {
tree: &'a SumTree<T>,
index: u32,
position: D,
}
impl<'a, T: Item, D> StackEntry<'a, T, D> {
#[inline]
fn index(&self) -> usize {
self.index as usize
}
}
impl<T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for StackEntry<'_, T, D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StackEntry")
.field("index", &self.index)
.field("position", &self.position)
.finish()
}
}
#[derive(Clone)]
pub struct Cursor<'a, 'b, T: Item, D> {
tree: &'a SumTree<T>,
stack: ArrayVec<StackEntry<'a, T, D>, 16>,
position: D,
did_seek: bool,
at_end: bool,
cx: <T::Summary as Summary>::Context<'b>,
}
impl<T: Item + fmt::Debug, D: fmt::Debug> fmt::Debug for Cursor<'_, '_, T, D>
where
T::Summary: fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Cursor")
.field("tree", &self.tree)
.field("stack", &self.stack)
.field("position", &self.position)
.field("did_seek", &self.did_seek)
.field("at_end", &self.at_end)
.finish()
}
}
pub struct Iter<'a, T: Item> {
tree: &'a SumTree<T>,
stack: ArrayVec<StackEntry<'a, T, ()>, 16>,
}
impl<'a, 'b, T, D> Cursor<'a, 'b, T, D>
where
T: Item,
D: Dimension<'a, T::Summary>,
{
pub fn new(tree: &'a SumTree<T>, cx: <T::Summary as Summary>::Context<'b>) -> Self {
Self {
tree,
stack: ArrayVec::new(),
position: D::zero(cx),
did_seek: false,
at_end: tree.is_empty(),
cx,
}
}
fn reset(&mut self) {
self.did_seek = false;
self.at_end = self.tree.is_empty();
self.stack.truncate(0);
self.position = D::zero(self.cx);
}
pub fn start(&self) -> &D {
&self.position
}
#[track_caller]
pub fn end(&self) -> D {
if let Some(item_summary) = self.item_summary() {
let mut end = self.start().clone();
end.add_summary(item_summary, self.cx);
end
} else {
self.start().clone()
}
}
/// Item is None, when the list is empty, or this cursor is at the end of the list.
#[track_caller]
pub fn item(&self) -> Option<&'a T> {
self.assert_did_seek();
if let Some(entry) = self.stack.last() {
match *entry.tree.0 {
Node::Leaf { ref items, .. } => {
if entry.index() == items.len() {
None
} else {
Some(&items[entry.index()])
}
}
_ => unreachable!(),
}
} else {
None
}
}
#[track_caller]
pub fn item_summary(&self) -> Option<&'a T::Summary> {
self.assert_did_seek();
if let Some(entry) = self.stack.last() {
match *entry.tree.0 {
Node::Leaf {
ref item_summaries, ..
} => {
if entry.index() == item_summaries.len() {
None
} else {
Some(&item_summaries[entry.index()])
}
}
_ => unreachable!(),
}
} else {
None
}
}
#[track_caller]
pub fn next_item(&self) -> Option<&'a T> {
self.assert_did_seek();
if let Some(entry) = self.stack.last() {
if entry.index() == entry.tree.0.items().len() - 1 {
if let Some(next_leaf) = self.next_leaf() {
Some(next_leaf.0.items().first().unwrap())
} else {
None
}
} else {
match *entry.tree.0 {
Node::Leaf { ref items, .. } => Some(&items[entry.index() + 1]),
_ => unreachable!(),
}
}
} else if self.at_end {
None
} else {
self.tree.first()
}
}
#[track_caller]
fn next_leaf(&self) -> Option<&'a SumTree<T>> {
for entry in self.stack.iter().rev().skip(1) {
if entry.index() < entry.tree.0.child_trees().len() - 1 {
match *entry.tree.0 {
Node::Internal {
ref child_trees, ..
} => return Some(child_trees[entry.index() + 1].leftmost_leaf()),
Node::Leaf { .. } => unreachable!(),
};
}
}
None
}
#[track_caller]
pub fn prev_item(&self) -> Option<&'a T> {
self.assert_did_seek();
if let Some(entry) = self.stack.last() {
if entry.index() == 0 {
if let Some(prev_leaf) = self.prev_leaf() {
Some(prev_leaf.0.items().last().unwrap())
} else {
None
}
} else {
match *entry.tree.0 {
Node::Leaf { ref items, .. } => Some(&items[entry.index() - 1]),
_ => unreachable!(),
}
}
} else if self.at_end {
self.tree.last()
} else {
None
}
}
#[track_caller]
fn prev_leaf(&self) -> Option<&'a SumTree<T>> {
for entry in self.stack.iter().rev().skip(1) {
if entry.index() != 0 {
match *entry.tree.0 {
Node::Internal {
ref child_trees, ..
} => return Some(child_trees[entry.index() - 1].rightmost_leaf()),
Node::Leaf { .. } => unreachable!(),
};
}
}
None
}
#[track_caller]
#[instrument(skip_all)]
pub fn prev(&mut self) {
self.search_backward(|_| true)
}
#[track_caller]
pub fn search_backward<F>(&mut self, mut filter_node: F)
where
F: FnMut(&T::Summary) -> bool,
{
if !self.did_seek {
self.did_seek = true;
self.at_end = true;
}
if self.at_end {
self.position = D::zero(self.cx);
self.at_end = self.tree.is_empty();
if !self.tree.is_empty() {
self.stack.push(StackEntry {
tree: self.tree,
index: self.tree.0.child_summaries().len() as u32,
position: D::from_summary(self.tree.summary(), self.cx),
});
}
}
let mut descending = false;
while !self.stack.is_empty() {
if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) {
self.position = position.clone();
} else {
self.position = D::zero(self.cx);
}
let entry = self.stack.last_mut().unwrap();
if !descending {
if entry.index() == 0 {
self.stack.pop();
continue;
} else {
entry.index -= 1;
}
}
for summary in &entry.tree.0.child_summaries()[..entry.index()] {
self.position.add_summary(summary, self.cx);
}
entry.position = self.position.clone();
descending = filter_node(&entry.tree.0.child_summaries()[entry.index()]);
match entry.tree.0.as_ref() {
Node::Internal { child_trees, .. } => {
if descending {
let tree = &child_trees[entry.index()];
self.stack.push(StackEntry {
position: D::zero(self.cx),
tree,
index: tree.0.child_summaries().len() as u32 - 1,
})
}
}
Node::Leaf { .. } => {
if descending {
break;
}
}
}
}
}
#[track_caller]
pub fn next(&mut self) {
self.search_forward(|_| true)
}
#[track_caller]
pub fn search_forward<F>(&mut self, mut filter_node: F)
where
F: FnMut(&T::Summary) -> bool,
{
let mut descend = false;
if self.stack.is_empty() {
if !self.at_end {
self.stack.push(StackEntry {
tree: self.tree,
index: 0,
position: D::zero(self.cx),
});
descend = true;
}
self.did_seek = true;
}
while !self.stack.is_empty() {
let new_subtree = {
let entry = self.stack.last_mut().unwrap();
match entry.tree.0.as_ref() {
Node::Internal {
child_trees,
child_summaries,
..
} => {
if !descend {
entry.index += 1;
entry.position = self.position.clone();
}
while entry.index() < child_summaries.len() {
let next_summary = &child_summaries[entry.index()];
if filter_node(next_summary) {
break;
} else {
entry.index += 1;
entry.position.add_summary(next_summary, self.cx);
self.position.add_summary(next_summary, self.cx);
}
}
child_trees.get(entry.index())
}
Node::Leaf { item_summaries, .. } => {
if !descend {
let item_summary = &item_summaries[entry.index()];
entry.index += 1;
entry.position.add_summary(item_summary, self.cx);
self.position.add_summary(item_summary, self.cx);
}
loop {
if let Some(next_item_summary) = item_summaries.get(entry.index()) {
if filter_node(next_item_summary) {
return;
} else {
entry.index += 1;
entry.position.add_summary(next_item_summary, self.cx);
self.position.add_summary(next_item_summary, self.cx);
}
} else {
break None;
}
}
}
}
};
if let Some(subtree) = new_subtree {
descend = true;
self.stack.push(StackEntry {
tree: subtree,
index: 0,
position: self.position.clone(),
});
} else {
descend = false;
self.stack.pop();
}
}
self.at_end = self.stack.is_empty();
debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf());
}
#[track_caller]
fn assert_did_seek(&self) {
assert!(
self.did_seek,
"Must call `seek`, `next` or `prev` before calling this method"
);
}
pub fn did_seek(&self) -> bool {
self.did_seek
}
}
impl<'a, 'b, T, D> Cursor<'a, 'b, T, D>
where
T: Item,
D: Dimension<'a, T::Summary>,
{
/// Returns whether we found the item you were seeking for.
#[track_caller]
#[instrument(skip_all)]
pub fn seek<Target>(&mut self, pos: &Target, bias: Bias) -> bool
where
Target: SeekTarget<'a, T::Summary, D>,
{
self.reset();
self.seek_internal(pos, bias, &mut ())
}
/// Returns whether we found the item you were seeking for.
///
/// # Panics
///
/// If we did not seek before, use seek instead in that case.
#[track_caller]
#[instrument(skip_all)]
pub fn seek_forward<Target>(&mut self, pos: &Target, bias: Bias) -> bool
where
Target: SeekTarget<'a, T::Summary, D>,
{
self.seek_internal(pos, bias, &mut ())
}
/// Advances the cursor and returns traversed items as a tree.
#[track_caller]
pub fn slice<Target>(&mut self, end: &Target, bias: Bias) -> SumTree<T>
where
Target: SeekTarget<'a, T::Summary, D>,
{
let mut slice = SliceSeekAggregate {
tree: SumTree::new(self.cx),
leaf_items: ArrayVec::new(),
leaf_item_summaries: ArrayVec::new(),
leaf_summary: <T::Summary as Summary>::zero(self.cx),
};
self.seek_internal(end, bias, &mut slice);
slice.tree
}
#[track_caller]
pub fn suffix(&mut self) -> SumTree<T> {
self.slice(&End::new(), Bias::Right)
}
#[track_caller]
pub fn summary<Target, Output>(&mut self, end: &Target, bias: Bias) -> Output
where
Target: SeekTarget<'a, T::Summary, D>,
Output: Dimension<'a, T::Summary>,
{
let mut summary = SummarySeekAggregate(Output::zero(self.cx));
self.seek_internal(end, bias, &mut summary);
summary.0
}
/// Returns whether we found the item you were seeking for.
#[track_caller]
#[instrument(skip_all)]
fn seek_internal(
&mut self,
target: &dyn SeekTarget<'a, T::Summary, D>,
bias: Bias,
aggregate: &mut dyn SeekAggregate<'a, T>,
) -> bool {
assert!(
target.cmp(&self.position, self.cx).is_ge(),
"cannot seek backward",
);
if !self.did_seek {
self.did_seek = true;
self.stack.push(StackEntry {
tree: self.tree,
index: 0,
position: D::zero(self.cx),
});
}
let mut ascending = false;
'outer: while let Some(entry) = self.stack.last_mut() {
match *entry.tree.0 {
Node::Internal {
ref child_summaries,
ref child_trees,
..
} => {
if ascending {
entry.index += 1;
entry.position = self.position.clone();
}
for (child_tree, child_summary) in child_trees[entry.index()..]
.iter()
.zip(&child_summaries[entry.index()..])
{
let mut child_end = self.position.clone();
child_end.add_summary(child_summary, self.cx);
let comparison = target.cmp(&child_end, self.cx);
if comparison == Ordering::Greater
|| (comparison == Ordering::Equal && bias == Bias::Right)
{
self.position = child_end;
aggregate.push_tree(child_tree, child_summary, self.cx);
entry.index += 1;
entry.position = self.position.clone();
} else {
self.stack.push(StackEntry {
tree: child_tree,
index: 0,
position: self.position.clone(),
});
ascending = false;
continue 'outer;
}
}
}
Node::Leaf {
ref items,
ref item_summaries,
..
} => {
aggregate.begin_leaf();
for (item, item_summary) in items[entry.index()..]
.iter()
.zip(&item_summaries[entry.index()..])
{
let mut child_end = self.position.clone();
child_end.add_summary(item_summary, self.cx);
let comparison = target.cmp(&child_end, self.cx);
if comparison == Ordering::Greater
|| (comparison == Ordering::Equal && bias == Bias::Right)
{
self.position = child_end;
aggregate.push_item(item, item_summary, self.cx);
entry.index += 1;
} else {
aggregate.end_leaf(self.cx);
break 'outer;
}
}
aggregate.end_leaf(self.cx);
}
}
self.stack.pop();
ascending = true;
}
self.at_end = self.stack.is_empty();
debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf());
let mut end = self.position.clone();
if bias == Bias::Left
&& let Some(summary) = self.item_summary()
{
end.add_summary(summary, self.cx);
}
target.cmp(&end, self.cx) == Ordering::Equal
}
}
impl<'a, T: Item> Iter<'a, T> {
pub(crate) fn new(tree: &'a SumTree<T>) -> Self {
Self {
tree,
stack: Default::default(),
}
}
}
impl<'a, T: Item> Iterator for Iter<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
let mut descend = false;
if self.stack.is_empty() {
self.stack.push(StackEntry {
tree: self.tree,
index: 0,
position: (),
});
descend = true;
}
while !self.stack.is_empty() {
let new_subtree = {
let entry = self.stack.last_mut().unwrap();
match entry.tree.0.as_ref() {
Node::Internal { child_trees, .. } => {
if !descend {
entry.index += 1;
}
child_trees.get(entry.index())
}
Node::Leaf { items, .. } => {
if !descend {
entry.index += 1;
}
if let Some(next_item) = items.get(entry.index()) {
return Some(next_item);
} else {
None
}
}
}
};
if let Some(subtree) = new_subtree {
descend = true;
self.stack.push(StackEntry {
tree: subtree,
index: 0,
position: (),
});
} else {
descend = false;
self.stack.pop();
}
}
None
}
}
impl<'a, 'b, T: Item, D> Iterator for Cursor<'a, 'b, T, D>
where
D: Dimension<'a, T::Summary>,
{
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if !self.did_seek {
self.next();
}
if let Some(item) = self.item() {
self.next();
Some(item)
} else {
None
}
}
}
pub struct FilterCursor<'a, 'b, F, T: Item, D> {
cursor: Cursor<'a, 'b, T, D>,
filter_node: F,
}
impl<'a, 'b, F, T: Item, D> FilterCursor<'a, 'b, F, T, D>
where
F: FnMut(&T::Summary) -> bool,
T: Item,
D: Dimension<'a, T::Summary>,
{
pub fn new(
tree: &'a SumTree<T>,
cx: <T::Summary as Summary>::Context<'b>,
filter_node: F,
) -> Self {
let cursor = tree.cursor::<D>(cx);
Self {
cursor,
filter_node,
}
}
pub fn start(&self) -> &D {
self.cursor.start()
}
pub fn end(&self) -> D {
self.cursor.end()
}
pub fn item(&self) -> Option<&'a T> {
self.cursor.item()
}
pub fn item_summary(&self) -> Option<&'a T::Summary> {
self.cursor.item_summary()
}
pub fn next(&mut self) {
self.cursor.search_forward(&mut self.filter_node);
}
pub fn prev(&mut self) {
self.cursor.search_backward(&mut self.filter_node);
}
}
impl<'a, 'b, F, T: Item, U> Iterator for FilterCursor<'a, 'b, F, T, U>
where
F: FnMut(&T::Summary) -> bool,
U: Dimension<'a, T::Summary>,
{
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
if !self.cursor.did_seek {
self.next();
}
if let Some(item) = self.item() {
self.cursor.search_forward(&mut self.filter_node);
Some(item)
} else {
None
}
}
}
trait SeekAggregate<'a, T: Item> {
fn begin_leaf(&mut self);
fn end_leaf(&mut self, cx: <T::Summary as Summary>::Context<'_>);
fn push_item(
&mut self,
item: &'a T,
summary: &'a T::Summary,
cx: <T::Summary as Summary>::Context<'_>,
);
fn push_tree(
&mut self,
tree: &'a SumTree<T>,
summary: &'a T::Summary,
cx: <T::Summary as Summary>::Context<'_>,
);
}
struct SliceSeekAggregate<T: Item> {
tree: SumTree<T>,
leaf_items: ArrayVec<T, { 2 * TREE_BASE }>,
leaf_item_summaries: ArrayVec<T::Summary, { 2 * TREE_BASE }>,
leaf_summary: T::Summary,
}
struct SummarySeekAggregate<D>(D);
impl<T: Item> SeekAggregate<'_, T> for () {
fn begin_leaf(&mut self) {}
fn end_leaf(&mut self, _: <T::Summary as Summary>::Context<'_>) {}
fn push_item(&mut self, _: &T, _: &T::Summary, _: <T::Summary as Summary>::Context<'_>) {}
fn push_tree(
&mut self,
_: &SumTree<T>,
_: &T::Summary,
_: <T::Summary as Summary>::Context<'_>,
) {
}
}
impl<T: Item> SeekAggregate<'_, T> for SliceSeekAggregate<T> {
fn begin_leaf(&mut self) {}
fn end_leaf(&mut self, cx: <T::Summary as Summary>::Context<'_>) {
self.tree.append(
SumTree(Arc::new(Node::Leaf {
summary: mem::replace(&mut self.leaf_summary, <T::Summary as Summary>::zero(cx)),
items: mem::take(&mut self.leaf_items),
item_summaries: mem::take(&mut self.leaf_item_summaries),
})),
cx,
);
}
fn push_item(
&mut self,
item: &T,
summary: &T::Summary,
cx: <T::Summary as Summary>::Context<'_>,
) {
self.leaf_items.push(item.clone());
self.leaf_item_summaries.push(summary.clone());
Summary::add_summary(&mut self.leaf_summary, summary, cx);
}
fn push_tree(
&mut self,
tree: &SumTree<T>,
_: &T::Summary,
cx: <T::Summary as Summary>::Context<'_>,
) {
self.tree.append(tree.clone(), cx);
}
}
impl<'a, T: Item, D> SeekAggregate<'a, T> for SummarySeekAggregate<D>
where
D: Dimension<'a, T::Summary>,
{
fn begin_leaf(&mut self) {}
fn end_leaf(&mut self, _: <T::Summary as Summary>::Context<'_>) {}
fn push_item(
&mut self,
_: &T,
summary: &'a T::Summary,
cx: <T::Summary as Summary>::Context<'_>,
) {
self.0.add_summary(summary, cx);
}
fn push_tree(
&mut self,
_: &SumTree<T>,
summary: &'a T::Summary,
cx: <T::Summary as Summary>::Context<'_>,
) {
self.0.add_summary(summary, cx);
}
}
struct End<D>(PhantomData<D>);
impl<D> End<D> {
fn new() -> Self {
Self(PhantomData)
}
}
impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End<D> {
fn cmp(&self, _: &D, _: S::Context<'_>) -> Ordering {
Ordering::Greater
}
}
impl<D> fmt::Debug for End<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("End").finish()
}
}
File diff suppressed because it is too large Load Diff
-527
View File
@@ -1,527 +0,0 @@
use std::{cmp::Ordering, fmt::Debug};
use crate::{Bias, ContextLessSummary, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree};
/// A cheaply-cloneable ordered map based on a [SumTree](crate::SumTree).
#[derive(Clone, PartialEq, Eq)]
pub struct TreeMap<K, V>(SumTree<MapEntry<K, V>>)
where
K: Clone + Ord,
V: Clone;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MapEntry<K, V> {
key: K,
value: V,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct MapKey<K>(Option<K>);
impl<K> Default for MapKey<K> {
fn default() -> Self {
Self(None)
}
}
#[derive(Clone, Debug)]
pub struct MapKeyRef<'a, K>(Option<&'a K>);
impl<K> Default for MapKeyRef<'_, K> {
fn default() -> Self {
Self(None)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TreeSet<K>(TreeMap<K, ()>)
where
K: Clone + Ord;
impl<K: Clone + Ord, V: Clone> TreeMap<K, V> {
pub fn from_ordered_entries(entries: impl IntoIterator<Item = (K, V)>) -> Self {
let tree = SumTree::from_iter(
entries
.into_iter()
.map(|(key, value)| MapEntry { key, value }),
(),
);
Self(tree)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn get(&self, key: &K) -> Option<&V> {
let (.., item) = self
.0
.find::<MapKeyRef<'_, K>, _>((), &MapKeyRef(Some(key)), Bias::Left);
if let Some(item) = item {
if Some(key) == item.key().0.as_ref() {
Some(&item.value)
} else {
None
}
} else {
None
}
}
pub fn insert(&mut self, key: K, value: V) {
self.0.insert_or_replace(MapEntry { key, value }, ());
}
pub fn insert_or_replace(&mut self, key: K, value: V) -> Option<V> {
self.0
.insert_or_replace(MapEntry { key, value }, ())
.map(|it| it.value)
}
pub fn extend(&mut self, iter: impl IntoIterator<Item = (K, V)>) {
let edits: Vec<_> = iter
.into_iter()
.map(|(key, value)| Edit::Insert(MapEntry { key, value }))
.collect();
self.0.edit(edits, ());
}
pub fn clear(&mut self) {
self.0 = SumTree::default();
}
pub fn remove(&mut self, key: &K) -> Option<V> {
let mut removed = None;
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
let key = MapKeyRef(Some(key));
let mut new_tree = cursor.slice(&key, Bias::Left);
if key.cmp(&cursor.end(), ()) == Ordering::Equal {
removed = Some(cursor.item().unwrap().value.clone());
cursor.next();
}
new_tree.append(cursor.suffix(), ());
drop(cursor);
self.0 = new_tree;
removed
}
pub fn remove_range(&mut self, start: &impl MapSeekTarget<K>, end: &impl MapSeekTarget<K>) {
let start = MapSeekTargetAdaptor(start);
let end = MapSeekTargetAdaptor(end);
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
let mut new_tree = cursor.slice(&start, Bias::Left);
cursor.seek(&end, Bias::Left);
new_tree.append(cursor.suffix(), ());
drop(cursor);
self.0 = new_tree;
}
/// Returns the key-value pair with the greatest key less than or equal to the given key.
pub fn closest(&self, key: &K) -> Option<(&K, &V)> {
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
let key = MapKeyRef(Some(key));
cursor.seek(&key, Bias::Right);
cursor.prev();
cursor.item().map(|item| (&item.key, &item.value))
}
pub fn iter_from<'a>(&'a self, from: &K) -> impl Iterator<Item = (&'a K, &'a V)> + 'a {
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
let from_key = MapKeyRef(Some(from));
cursor.seek(&from_key, Bias::Left);
cursor.map(|map_entry| (&map_entry.key, &map_entry.value))
}
pub fn update<F, T>(&mut self, key: &K, f: F) -> Option<T>
where
F: FnOnce(&mut V) -> T,
{
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
let key = MapKeyRef(Some(key));
let mut new_tree = cursor.slice(&key, Bias::Left);
let mut result = None;
if key.cmp(&cursor.end(), ()) == Ordering::Equal {
let mut updated = cursor.item().unwrap().clone();
result = Some(f(&mut updated.value));
new_tree.push(updated, ());
cursor.next();
}
new_tree.append(cursor.suffix(), ());
drop(cursor);
self.0 = new_tree;
result
}
pub fn retain<F: FnMut(&K, &V) -> bool>(&mut self, mut predicate: F) {
let mut new_map = SumTree::<MapEntry<K, V>>::default();
let mut cursor = self.0.cursor::<MapKeyRef<'_, K>>(());
cursor.next();
while let Some(item) = cursor.item() {
if predicate(&item.key, &item.value) {
new_map.push(item.clone(), ());
}
cursor.next();
}
drop(cursor);
self.0 = new_map;
}
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> + '_ {
self.0.iter().map(|entry| (&entry.key, &entry.value))
}
pub fn values(&self) -> impl Iterator<Item = &V> + '_ {
self.0.iter().map(|entry| &entry.value)
}
pub fn first(&self) -> Option<(&K, &V)> {
self.0.first().map(|entry| (&entry.key, &entry.value))
}
pub fn last(&self) -> Option<(&K, &V)> {
self.0.last().map(|entry| (&entry.key, &entry.value))
}
pub fn insert_tree(&mut self, other: TreeMap<K, V>) {
let edits = other
.iter()
.map(|(key, value)| {
Edit::Insert(MapEntry {
key: key.to_owned(),
value: value.to_owned(),
})
})
.collect();
self.0.edit(edits, ());
}
}
impl<K, V> Debug for TreeMap<K, V>
where
K: Clone + Debug + Ord,
V: Clone + Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map().entries(self.iter()).finish()
}
}
#[derive(Debug)]
struct MapSeekTargetAdaptor<'a, T>(&'a T);
impl<'a, K: Clone + Ord, T: MapSeekTarget<K>> SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>>
for MapSeekTargetAdaptor<'_, T>
{
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: ()) -> Ordering {
if let Some(key) = &cursor_location.0 {
MapSeekTarget::cmp_cursor(self.0, key)
} else {
Ordering::Greater
}
}
}
pub trait MapSeekTarget<K> {
fn cmp_cursor(&self, cursor_location: &K) -> Ordering;
}
impl<K: Ord> MapSeekTarget<K> for K {
fn cmp_cursor(&self, cursor_location: &K) -> Ordering {
self.cmp(cursor_location)
}
}
impl<K, V> Default for TreeMap<K, V>
where
K: Clone + Ord,
V: Clone,
{
fn default() -> Self {
Self(Default::default())
}
}
impl<K, V> Item for MapEntry<K, V>
where
K: Clone + Ord,
V: Clone,
{
type Summary = MapKey<K>;
fn summary(&self, _cx: ()) -> Self::Summary {
self.key()
}
}
impl<K, V> KeyedItem for MapEntry<K, V>
where
K: Clone + Ord,
V: Clone,
{
type Key = MapKey<K>;
fn key(&self) -> Self::Key {
MapKey(Some(self.key.clone()))
}
}
impl<K> ContextLessSummary for MapKey<K>
where
K: Clone,
{
fn zero() -> Self {
Default::default()
}
fn add_summary(&mut self, summary: &Self) {
*self = summary.clone()
}
}
impl<'a, K> Dimension<'a, MapKey<K>> for MapKeyRef<'a, K>
where
K: Clone + Ord,
{
fn zero(_cx: ()) -> Self {
Default::default()
}
fn add_summary(&mut self, summary: &'a MapKey<K>, _: ()) {
self.0 = summary.0.as_ref();
}
}
impl<'a, K> SeekTarget<'a, MapKey<K>, MapKeyRef<'a, K>> for MapKeyRef<'_, K>
where
K: Clone + Ord,
{
fn cmp(&self, cursor_location: &MapKeyRef<K>, _: ()) -> Ordering {
Ord::cmp(&self.0, &cursor_location.0)
}
}
impl<K> Default for TreeSet<K>
where
K: Clone + Ord,
{
fn default() -> Self {
Self(Default::default())
}
}
impl<K> TreeSet<K>
where
K: Clone + Ord,
{
pub fn from_ordered_entries(entries: impl IntoIterator<Item = K>) -> Self {
Self(TreeMap::from_ordered_entries(
entries.into_iter().map(|key| (key, ())),
))
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn insert(&mut self, key: K) {
self.0.insert(key, ());
}
pub fn remove(&mut self, key: &K) -> bool {
self.0.remove(key).is_some()
}
pub fn extend(&mut self, iter: impl IntoIterator<Item = K>) {
self.0.extend(iter.into_iter().map(|key| (key, ())));
}
pub fn contains(&self, key: &K) -> bool {
self.0.get(key).is_some()
}
pub fn iter(&self) -> impl Iterator<Item = &K> + '_ {
self.0.iter().map(|(k, _)| k)
}
pub fn iter_from<'a>(&'a self, key: &K) -> impl Iterator<Item = &'a K> + 'a {
self.0.iter_from(key).map(move |(k, _)| k)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic() {
let mut map = TreeMap::default();
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
map.insert(3, "c");
assert_eq!(map.get(&3), Some(&"c"));
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&3, &"c")]);
map.insert(1, "a");
assert_eq!(map.get(&1), Some(&"a"));
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
map.insert(2, "b");
assert_eq!(map.get(&2), Some(&"b"));
assert_eq!(map.get(&1), Some(&"a"));
assert_eq!(map.get(&3), Some(&"c"));
assert_eq!(
map.iter().collect::<Vec<_>>(),
vec![(&1, &"a"), (&2, &"b"), (&3, &"c")]
);
assert_eq!(map.closest(&0), None);
assert_eq!(map.closest(&1), Some((&1, &"a")));
assert_eq!(map.closest(&10), Some((&3, &"c")));
map.remove(&2);
assert_eq!(map.get(&2), None);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a"), (&3, &"c")]);
assert_eq!(map.closest(&2), Some((&1, &"a")));
map.remove(&3);
assert_eq!(map.get(&3), None);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&1, &"a")]);
map.remove(&1);
assert_eq!(map.get(&1), None);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![]);
map.insert(4, "d");
map.insert(5, "e");
map.insert(6, "f");
map.retain(|key, _| *key % 2 == 0);
assert_eq!(map.iter().collect::<Vec<_>>(), vec![(&4, &"d"), (&6, &"f")]);
}
#[test]
fn test_iter_from() {
let mut map = TreeMap::default();
map.insert("a", 1);
map.insert("b", 2);
map.insert("baa", 3);
map.insert("baaab", 4);
map.insert("c", 5);
let result = map
.iter_from(&"ba")
.take_while(|(key, _)| key.starts_with("ba"))
.collect::<Vec<_>>();
assert_eq!(result.len(), 2);
assert!(result.iter().any(|(k, _)| k == &&"baa"));
assert!(result.iter().any(|(k, _)| k == &&"baaab"));
let result = map
.iter_from(&"c")
.take_while(|(key, _)| key.starts_with("c"))
.collect::<Vec<_>>();
assert_eq!(result.len(), 1);
assert!(result.iter().any(|(k, _)| k == &&"c"));
}
#[test]
fn test_insert_tree() {
let mut map = TreeMap::default();
map.insert("a", 1);
map.insert("b", 2);
map.insert("c", 3);
let mut other = TreeMap::default();
other.insert("a", 2);
other.insert("b", 2);
other.insert("d", 4);
map.insert_tree(other);
assert_eq!(map.iter().count(), 4);
assert_eq!(map.get(&"a"), Some(&2));
assert_eq!(map.get(&"b"), Some(&2));
assert_eq!(map.get(&"c"), Some(&3));
assert_eq!(map.get(&"d"), Some(&4));
}
#[test]
fn test_extend() {
let mut map = TreeMap::default();
map.insert("a", 1);
map.insert("b", 2);
map.insert("c", 3);
map.extend([("a", 2), ("b", 2), ("d", 4)]);
assert_eq!(map.iter().count(), 4);
assert_eq!(map.get(&"a"), Some(&2));
assert_eq!(map.get(&"b"), Some(&2));
assert_eq!(map.get(&"c"), Some(&3));
assert_eq!(map.get(&"d"), Some(&4));
}
#[test]
fn test_remove_between_and_path_successor() {
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct PathDescendants<'a>(&'a Path);
impl MapSeekTarget<PathBuf> for PathDescendants<'_> {
fn cmp_cursor(&self, key: &PathBuf) -> Ordering {
if key.starts_with(self.0) {
Ordering::Greater
} else {
self.0.cmp(key)
}
}
}
let mut map = TreeMap::default();
map.insert(PathBuf::from("a"), 1);
map.insert(PathBuf::from("a/a"), 1);
map.insert(PathBuf::from("b"), 2);
map.insert(PathBuf::from("b/a/a"), 3);
map.insert(PathBuf::from("b/a/a/a/b"), 4);
map.insert(PathBuf::from("c"), 5);
map.insert(PathBuf::from("c/a"), 6);
map.remove_range(
&PathBuf::from("b/a"),
&PathDescendants(&PathBuf::from("b/a")),
);
assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
assert_eq!(map.get(&PathBuf::from("b/a/a")), None);
assert_eq!(map.get(&PathBuf::from("b/a/a/a/b")), None);
assert_eq!(map.get(&PathBuf::from("c")), Some(&5));
assert_eq!(map.get(&PathBuf::from("c/a")), Some(&6));
map.remove_range(&PathBuf::from("c"), &PathDescendants(&PathBuf::from("c")));
assert_eq!(map.get(&PathBuf::from("a")), Some(&1));
assert_eq!(map.get(&PathBuf::from("a/a")), Some(&1));
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
assert_eq!(map.get(&PathBuf::from("c")), None);
assert_eq!(map.get(&PathBuf::from("c/a")), None);
map.remove_range(&PathBuf::from("a"), &PathDescendants(&PathBuf::from("a")));
assert_eq!(map.get(&PathBuf::from("a")), None);
assert_eq!(map.get(&PathBuf::from("a/a")), None);
assert_eq!(map.get(&PathBuf::from("b")), Some(&2));
map.remove_range(&PathBuf::from("b"), &PathDescendants(&PathBuf::from("b")));
assert_eq!(map.get(&PathBuf::from("b")), None);
}
}
-65
View File
@@ -1,65 +0,0 @@
[package]
name = "util"
version = "0.1.0"
edition.workspace = true
publish = false
license = "Apache-2.0"
description = "A collection of utility structs and functions used by Zed and GPUI"
[lints]
workspace = true
[lib]
path = "src/util.rs"
doctest = true
[features]
test-support = ["git2", "rand", "util_macros"]
[dependencies]
anyhow.workspace = true
async-fs.workspace = true
async_zip.workspace = true
collections.workspace = true
dirs.workspace = true
dunce = "1.0"
futures-lite.workspace = true
futures.workspace = true
git2 = { workspace = true, optional = true }
globset.workspace = true
itertools.workspace = true
log.workspace = true
rand = { workspace = true, optional = true }
regex.workspace = true
rust-embed.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
shlex.workspace = true
smol.workspace = true
take-until.workspace = true
tempfile.workspace = true
unicase.workspace = true
util_macros = { workspace = true, optional = true }
walkdir.workspace = true
which.workspace = true
[target.'cfg(unix)'.dependencies]
command-fds = "0.3.1"
libc.workspace = true
nix = { workspace = true, features = ["user"] }
[target.'cfg(target_os = "macos")'.dependencies]
mach2.workspace = true
[target.'cfg(windows)'.dependencies]
tendril = "0.4.3"
[dev-dependencies]
git2.workspace = true
indoc.workspace = true
rand.workspace = true
util_macros.workspace = true
pretty_assertions.workspace = true
-1
View File
@@ -1 +0,0 @@
../../LICENSE-APACHE
-141
View File
@@ -1,141 +0,0 @@
use std::{
borrow::Cow,
cmp::Ordering,
fmt::{self, Debug},
hash::{Hash, Hasher},
sync::Arc,
};
pub enum ArcCow<'a, T: ?Sized> {
Borrowed(&'a T),
Owned(Arc<T>),
}
impl<T: ?Sized + PartialEq> PartialEq for ArcCow<'_, T> {
fn eq(&self, other: &Self) -> bool {
let a = self.as_ref();
let b = other.as_ref();
a == b
}
}
impl<T: ?Sized + PartialOrd> PartialOrd for ArcCow<'_, T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
self.as_ref().partial_cmp(other.as_ref())
}
}
impl<T: ?Sized + Ord> Ord for ArcCow<'_, T> {
fn cmp(&self, other: &Self) -> Ordering {
self.as_ref().cmp(other.as_ref())
}
}
impl<T: ?Sized + Eq> Eq for ArcCow<'_, T> {}
impl<T: ?Sized + Hash> Hash for ArcCow<'_, T> {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Self::Borrowed(borrowed) => Hash::hash(borrowed, state),
Self::Owned(owned) => Hash::hash(&**owned, state),
}
}
}
impl<T: ?Sized> Clone for ArcCow<'_, T> {
fn clone(&self) -> Self {
match self {
Self::Borrowed(borrowed) => Self::Borrowed(borrowed),
Self::Owned(owned) => Self::Owned(owned.clone()),
}
}
}
impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> {
fn from(s: &'a T) -> Self {
Self::Borrowed(s)
}
}
impl<T: ?Sized> From<Arc<T>> for ArcCow<'_, T> {
fn from(s: Arc<T>) -> Self {
Self::Owned(s)
}
}
impl<T: ?Sized> From<&'_ Arc<T>> for ArcCow<'_, T> {
fn from(s: &'_ Arc<T>) -> Self {
Self::Owned(s.clone())
}
}
impl From<String> for ArcCow<'_, str> {
fn from(value: String) -> Self {
Self::Owned(value.into())
}
}
impl From<&String> for ArcCow<'_, str> {
fn from(value: &String) -> Self {
Self::Owned(value.clone().into())
}
}
impl<'a> From<Cow<'a, str>> for ArcCow<'a, str> {
fn from(value: Cow<'a, str>) -> Self {
match value {
Cow::Borrowed(borrowed) => Self::Borrowed(borrowed),
Cow::Owned(owned) => Self::Owned(owned.into()),
}
}
}
impl<T> From<Vec<T>> for ArcCow<'_, [T]> {
fn from(vec: Vec<T>) -> Self {
ArcCow::Owned(Arc::from(vec))
}
}
impl<'a> From<&'a str> for ArcCow<'a, [u8]> {
fn from(s: &'a str) -> Self {
ArcCow::Borrowed(s.as_bytes())
}
}
impl<T: ?Sized + ToOwned> std::borrow::Borrow<T> for ArcCow<'_, T> {
fn borrow(&self) -> &T {
match self {
ArcCow::Borrowed(borrowed) => borrowed,
ArcCow::Owned(owned) => owned.as_ref(),
}
}
}
impl<T: ?Sized> std::ops::Deref for ArcCow<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
match self {
ArcCow::Borrowed(s) => s,
ArcCow::Owned(s) => s.as_ref(),
}
}
}
impl<T: ?Sized> AsRef<T> for ArcCow<'_, T> {
fn as_ref(&self) -> &T {
match self {
ArcCow::Borrowed(borrowed) => borrowed,
ArcCow::Owned(owned) => owned.as_ref(),
}
}
}
impl<T: ?Sized + Debug> Debug for ArcCow<'_, T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ArcCow::Borrowed(borrowed) => Debug::fmt(borrowed, f),
ArcCow::Owned(owned) => Debug::fmt(&**owned, f),
}
}
}
-254
View File
@@ -1,254 +0,0 @@
use std::path::Path;
use anyhow::{Context as _, Result};
use async_zip::base::read;
#[cfg(not(windows))]
use futures::AsyncSeek;
use futures::{AsyncRead, io::BufReader};
#[cfg(windows)]
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
let mut reader = read::stream::ZipFileReader::new(BufReader::new(reader));
let destination = &destination
.canonicalize()
.unwrap_or_else(|_| destination.to_path_buf());
while let Some(mut item) = reader.next_with_entry().await? {
let entry_reader = item.reader_mut();
let entry = entry_reader.entry();
let path = destination.join(
entry
.filename()
.as_str()
.context("reading zip entry file name")?,
);
if entry
.dir()
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
{
std::fs::create_dir_all(&path)
.with_context(|| format!("creating directory {path:?}"))?;
} else {
let parent_dir = path
.parent()
.with_context(|| format!("no parent directory for {path:?}"))?;
std::fs::create_dir_all(parent_dir)
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
let mut file = smol::fs::File::create(&path)
.await
.with_context(|| format!("creating file {path:?}"))?;
futures::io::copy(entry_reader, &mut file)
.await
.with_context(|| format!("extracting into file {path:?}"))?;
}
reader = item.skip().await.context("reading next zip entry")?;
}
Ok(())
}
#[cfg(not(windows))]
pub async fn extract_zip<R: AsyncRead + Unpin>(destination: &Path, reader: R) -> Result<()> {
// Unix needs file permissions copied when extracting.
// This is only possible to do when a reader impls `AsyncSeek` and `seek::ZipFileReader` is used.
// `stream::ZipFileReader` also has the `unix_permissions` method, but it will always return `Some(0)`.
//
// A typical `reader` comes from a streaming network response, so cannot be sought right away,
// and reading the entire archive into the memory seems wasteful.
//
// So, save the stream into a temporary file first and then get it read with a seeking reader.
let mut file = async_fs::File::from(tempfile::tempfile().context("creating a temporary file")?);
futures::io::copy(&mut BufReader::new(reader), &mut file)
.await
.context("saving archive contents into the temporary file")?;
extract_seekable_zip(destination, file).await
}
#[cfg(not(windows))]
pub async fn extract_seekable_zip<R: AsyncRead + AsyncSeek + Unpin>(
destination: &Path,
reader: R,
) -> Result<()> {
let mut reader = read::seek::ZipFileReader::new(BufReader::new(reader))
.await
.context("reading the zip archive")?;
let destination = &destination
.canonicalize()
.unwrap_or_else(|_| destination.to_path_buf());
for (i, entry) in reader.file().entries().to_vec().into_iter().enumerate() {
let path = destination.join(
entry
.filename()
.as_str()
.context("reading zip entry file name")?,
);
if entry
.dir()
.with_context(|| format!("reading zip entry metadata for path {path:?}"))?
{
std::fs::create_dir_all(&path)
.with_context(|| format!("creating directory {path:?}"))?;
} else {
let parent_dir = path
.parent()
.with_context(|| format!("no parent directory for {path:?}"))?;
std::fs::create_dir_all(parent_dir)
.with_context(|| format!("creating parent directory {parent_dir:?}"))?;
let mut file = smol::fs::File::create(&path)
.await
.with_context(|| format!("creating file {path:?}"))?;
let mut entry_reader = reader
.reader_with_entry(i)
.await
.with_context(|| format!("reading entry for path {path:?}"))?;
futures::io::copy(&mut entry_reader, &mut file)
.await
.with_context(|| format!("extracting into file {path:?}"))?;
if let Some(perms) = entry.unix_permissions() {
use std::os::unix::fs::PermissionsExt;
let permissions = std::fs::Permissions::from_mode(u32::from(perms));
file.set_permissions(permissions)
.await
.with_context(|| format!("setting permissions for file {path:?}"))?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use async_zip::ZipEntryBuilder;
use async_zip::base::write::ZipFileWriter;
use futures::{AsyncSeek, AsyncWriteExt};
use smol::io::Cursor;
use tempfile::TempDir;
use super::*;
async fn compress_zip(src_dir: &Path, dst: &Path) -> Result<()> {
let mut out = smol::fs::File::create(dst).await?;
let mut writer = ZipFileWriter::new(&mut out);
for entry in walkdir::WalkDir::new(src_dir) {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
continue;
}
let relative_path = path.strip_prefix(src_dir)?;
let data = smol::fs::read(&path).await?;
let filename = relative_path.display().to_string();
#[cfg(unix)]
{
let mut builder =
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
use std::os::unix::fs::PermissionsExt;
let metadata = std::fs::metadata(path)?;
let perms = metadata.permissions().mode() as u16;
builder = builder.unix_permissions(perms);
writer.write_entry_whole(builder, &data).await?;
}
#[cfg(not(unix))]
{
let builder =
ZipEntryBuilder::new(filename.into(), async_zip::Compression::Deflate);
writer.write_entry_whole(builder, &data).await?;
}
}
writer.close().await?;
out.flush().await?;
out.sync_all().await?;
Ok(())
}
#[track_caller]
fn assert_file_content(path: &Path, content: &str) {
assert!(path.exists(), "file not found: {:?}", path);
let actual = std::fs::read_to_string(path).unwrap();
assert_eq!(actual, content);
}
#[track_caller]
fn make_test_data() -> TempDir {
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
std::fs::write(dst.join("test"), "Hello world.").unwrap();
std::fs::create_dir_all(dst.join("foo/bar")).unwrap();
std::fs::write(dst.join("foo/bar.txt"), "Foo bar.").unwrap();
std::fs::write(dst.join("foo/dar.md"), "Bar dar.").unwrap();
std::fs::write(dst.join("foo/bar/dar你好.txt"), "你好世界").unwrap();
dir
}
async fn read_archive(path: &Path) -> impl AsyncRead + AsyncSeek + Unpin {
let data = smol::fs::read(&path).await.unwrap();
Cursor::new(data)
}
#[test]
fn test_extract_zip() {
let test_dir = make_test_data();
let zip_file = test_dir.path().join("test.zip");
smol::block_on(async {
compress_zip(test_dir.path(), &zip_file).await.unwrap();
let reader = read_archive(&zip_file).await;
let dir = tempfile::tempdir().unwrap();
let dst = dir.path();
extract_zip(dst, reader).await.unwrap();
assert_file_content(&dst.join("test"), "Hello world.");
assert_file_content(&dst.join("foo/bar.txt"), "Foo bar.");
assert_file_content(&dst.join("foo/dar.md"), "Bar dar.");
assert_file_content(&dst.join("foo/bar/dar你好.txt"), "你好世界");
});
}
#[cfg(unix)]
#[test]
fn test_extract_zip_preserves_executable_permissions() {
use std::os::unix::fs::PermissionsExt;
smol::block_on(async {
let test_dir = tempfile::tempdir().unwrap();
let executable_path = test_dir.path().join("my_script");
// Create an executable file
std::fs::write(&executable_path, "#!/bin/bash\necho 'Hello'").unwrap();
let mut perms = std::fs::metadata(&executable_path).unwrap().permissions();
perms.set_mode(0o755); // rwxr-xr-x
std::fs::set_permissions(&executable_path, perms).unwrap();
// Create zip
let zip_file = test_dir.path().join("test.zip");
compress_zip(test_dir.path(), &zip_file).await.unwrap();
// Extract to new location
let extract_dir = tempfile::tempdir().unwrap();
let reader = read_archive(&zip_file).await;
extract_zip(extract_dir.path(), reader).await.unwrap();
// Check permissions are preserved
let extracted_path = extract_dir.path().join("my_script");
assert!(extracted_path.exists());
let extracted_perms = std::fs::metadata(&extracted_path).unwrap().permissions();
assert_eq!(extracted_perms.mode() & 0o777, 0o755);
});
}
}
-102
View File
@@ -1,102 +0,0 @@
use std::ffi::OsStr;
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x0800_0000_u32;
#[cfg(target_os = "windows")]
pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
use std::os::windows::process::CommandExt;
let mut command = std::process::Command::new(program);
command.creation_flags(CREATE_NO_WINDOW);
command
}
#[cfg(not(target_os = "windows"))]
pub fn new_std_command(program: impl AsRef<OsStr>) -> std::process::Command {
std::process::Command::new(program)
}
#[cfg(target_os = "windows")]
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
use smol::process::windows::CommandExt;
let mut command = smol::process::Command::new(program);
command.creation_flags(CREATE_NO_WINDOW);
command
}
#[cfg(target_os = "macos")]
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
use std::os::unix::process::CommandExt;
// Create a std::process::Command first so we can use pre_exec
let mut std_cmd = std::process::Command::new(program);
// WORKAROUND: Reset exception ports before exec to prevent inheritance of
// crash handler exception ports. Due to a timing issue, child processes can
// inherit the parent's exception ports before they're fully stabilized,
// which can block child process spawning.
// See: https://github.com/zed-industries/zed/issues/36754
unsafe {
std_cmd.pre_exec(|| {
// Reset all exception ports to system defaults for this task.
// This prevents the child from inheriting the parent's crash handler
// exception ports.
reset_exception_ports();
Ok(())
});
}
// Convert to async_process::Command via From trait
smol::process::Command::from(std_cmd)
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
pub fn new_smol_command(program: impl AsRef<OsStr>) -> smol::process::Command {
smol::process::Command::new(program)
}
#[cfg(target_os = "macos")]
pub fn reset_exception_ports() {
use mach2::exception_types::{
EXC_MASK_ALL, EXCEPTION_DEFAULT, exception_behavior_t, exception_mask_t,
};
use mach2::kern_return::{KERN_SUCCESS, kern_return_t};
use mach2::mach_types::task_t;
use mach2::port::{MACH_PORT_NULL, mach_port_t};
use mach2::thread_status::{THREAD_STATE_NONE, thread_state_flavor_t};
use mach2::traps::mach_task_self;
// FFI binding for task_set_exception_ports (not exposed by mach2 crate)
unsafe extern "C" {
fn task_set_exception_ports(
task: task_t,
exception_mask: exception_mask_t,
new_port: mach_port_t,
behavior: exception_behavior_t,
new_flavor: thread_state_flavor_t,
) -> kern_return_t;
}
unsafe {
let task = mach_task_self();
// Reset all exception ports to MACH_PORT_NULL (system default)
// This prevents the child process from inheriting the parent's crash handler
let kr = task_set_exception_ports(
task,
EXC_MASK_ALL,
MACH_PORT_NULL,
EXCEPTION_DEFAULT as exception_behavior_t,
THREAD_STATE_NONE,
);
if kr != KERN_SUCCESS {
// Log but don't fail - the process can still work without this workaround
eprintln!(
"Warning: failed to reset exception ports in child process (kern_return: {})",
kr
);
}
}
}
-111
View File
@@ -1,111 +0,0 @@
use crate::ResultExt;
use anyhow::{Result, bail};
use async_fs as fs;
use futures_lite::StreamExt;
use std::path::{Path, PathBuf};
/// Removes all files and directories matching the given predicate
pub async fn remove_matching<F>(dir: &Path, predicate: F)
where
F: Fn(&Path) -> bool,
{
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if predicate(entry_path.as_path())
&& let Ok(metadata) = fs::metadata(&entry_path).await
{
if metadata.is_file() {
fs::remove_file(&entry_path).await.log_err();
} else {
fs::remove_dir_all(&entry_path).await.log_err();
}
}
}
}
}
}
pub async fn collect_matching<F>(dir: &Path, predicate: F) -> Vec<PathBuf>
where
F: Fn(&Path) -> bool,
{
let mut matching = vec![];
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err()
&& predicate(entry.path().as_path())
{
matching.push(entry.path());
}
}
}
matching
}
pub async fn find_file_name_in_dir<F>(dir: &Path, predicate: F) -> Option<PathBuf>
where
F: Fn(&str) -> bool,
{
if let Some(mut entries) = fs::read_dir(dir).await.log_err() {
while let Some(entry) = entries.next().await {
if let Some(entry) = entry.log_err() {
let entry_path = entry.path();
if let Some(file_name) = entry_path
.file_name()
.map(|file_name| file_name.to_string_lossy())
&& predicate(&file_name)
{
return Some(entry_path);
}
}
}
}
None
}
pub async fn move_folder_files_to_folder<P: AsRef<Path>>(
source_path: P,
target_path: P,
) -> Result<()> {
if !target_path.as_ref().is_dir() {
bail!("Folder not found or is not a directory");
}
let mut entries = fs::read_dir(source_path.as_ref()).await?;
while let Some(entry) = entries.next().await {
let entry = entry?;
let old_path = entry.path();
let new_path = target_path.as_ref().join(entry.file_name());
fs::rename(&old_path, &new_path).await?;
}
fs::remove_dir(source_path).await?;
Ok(())
}
#[cfg(unix)]
/// Set the permissions for the given path so that the file becomes executable.
/// This is a noop for non-unix platforms.
pub async fn make_file_executable(path: &Path) -> std::io::Result<()> {
fs::set_permissions(
path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await
}
#[cfg(not(unix))]
#[allow(clippy::unused_async)]
/// Set the permissions for the given path so that the file becomes executable.
/// This is a noop for non-unix platforms.
pub async fn make_file_executable(_path: &Path) -> std::io::Result<()> {
Ok(())
}
-268
View File
@@ -1,268 +0,0 @@
use std::fmt::{Display, Formatter};
/// Indicates that the wrapped `String` is markdown text.
#[derive(Debug, Clone)]
pub struct MarkdownString(pub String);
impl Display for MarkdownString {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
/// Escapes markdown special characters in markdown text blocks. Markdown code blocks follow
/// different rules and `MarkdownInlineCode` or `MarkdownCodeBlock` should be used in that case.
///
/// Also escapes the following markdown extensions:
///
/// * `^` for superscripts
/// * `$` for inline math
/// * `~` for strikethrough
///
/// Escape of some characters is unnecessary, because while they are involved in markdown syntax,
/// the other characters involved are escaped:
///
/// * `!`, `]`, `(`, and `)` are used in link syntax, but `[` is escaped so these are parsed as
/// plaintext.
///
/// * `;` is used in HTML entity syntax, but `&` is escaped, so they are parsed as plaintext.
///
/// TODO: There is one escape this doesn't do currently. Period after numbers at the start of the
/// line (`[0-9]*\.`) should also be escaped to avoid it being interpreted as a list item.
pub struct MarkdownEscaped<'a>(pub &'a str);
/// Implements `Display` to format markdown inline code (wrapped in backticks), handling code that
/// contains backticks and spaces. All whitespace is treated as a single space character. For text
/// that does not contain whitespace other than ' ', this escaping roundtrips through
/// pulldown-cmark.
///
/// When used in tables, `|` should be escaped like `\|` in the text provided to this function.
pub struct MarkdownInlineCode<'a>(pub &'a str);
/// Implements `Display` to format markdown code blocks, wrapped in 3 or more backticks as needed.
pub struct MarkdownCodeBlock<'a> {
pub tag: &'a str,
pub text: &'a str,
}
impl Display for MarkdownEscaped<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let mut start_of_unescaped = None;
for (ix, c) in self.0.char_indices() {
match c {
// Always escaped.
'\\' | '`' | '*' | '_' | '[' | '^' | '$' | '~' | '&' |
// TODO: these only need to be escaped when they are the first non-whitespace
// character of the line of a block. There should probably be both an `escape_block`
// which does this and an `escape_inline` method which does not escape these.
'#' | '+' | '=' | '-' => {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
write!(formatter, "\\")?;
// Can include this char in the "unescaped" text since a
// backslash was just emitted.
start_of_unescaped = Some(ix);
}
// Escaped since `<` is used in opening HTML tags. `&lt;` is used since Markdown
// supports HTML entities, and this allows the text to be used directly in HTML.
'<' => {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
write!(formatter, "&lt;")?;
start_of_unescaped = None;
}
// Escaped since `>` is used for blockquotes. `&gt;` is used since Markdown supports
// HTML entities, and this allows the text to be used directly in HTML.
'>' => {
match start_of_unescaped {
None => {}
Some(start_of_unescaped) => {
write!(formatter, "{}", &self.0[start_of_unescaped..ix])?;
}
}
write!(formatter, "&gt;")?;
start_of_unescaped = None;
}
_ => {
if start_of_unescaped.is_none() {
start_of_unescaped = Some(ix);
}
}
}
}
if let Some(start_of_unescaped) = start_of_unescaped {
write!(formatter, "{}", &self.0[start_of_unescaped..])?;
}
Ok(())
}
}
impl Display for MarkdownInlineCode<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
// Apache License 2.0, same as this crate.
//
// Copied from `pulldown-cmark-to-cmark-20.0.0` with modifications:
//
// * Handling of all whitespace. pulldown-cmark-to-cmark is anticipating
// `Code` events parsed by pulldown-cmark.
//
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L290
let mut all_whitespace = true;
let text = self
.0
.chars()
.map(|c| {
if c.is_whitespace() {
' '
} else {
all_whitespace = false;
c
}
})
.collect::<String>();
// When inline code has leading and trailing ' ' characters, additional space is needed
// to escape it, unless all characters are space.
if all_whitespace {
write!(formatter, "`{text}`")
} else {
// More backticks are needed to delimit the inline code than the maximum number of
// backticks in a consecutive run.
let backticks = "`".repeat(count_max_consecutive_chars(&text, '`') + 1);
let space = match text.as_bytes() {
&[b'`', ..] | &[.., b'`'] => " ", // Space needed to separate backtick.
&[b' ', .., b' '] => " ", // Space needed to escape inner space.
_ => "", // No space needed.
};
write!(formatter, "{backticks}{space}{text}{space}{backticks}")
}
}
}
impl Display for MarkdownCodeBlock<'_> {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
let tag = self.tag;
let text = self.text;
let backticks = "`".repeat(3.max(count_max_consecutive_chars(text, '`') + 1));
write!(formatter, "{backticks}{tag}\n{text}\n{backticks}\n")
}
}
// Copied from `pulldown-cmark-to-cmark-20.0.0` with changed names.
// https://github.com/Byron/pulldown-cmark-to-cmark/blob/3c850de2d3d1d79f19ca5f375e1089a653cf3ff7/src/lib.rs#L1063
// Apache License 2.0, same as this code.
fn count_max_consecutive_chars(text: &str, search: char) -> usize {
let mut in_search_chars = false;
let mut max_count = 0;
let mut cur_count = 0;
for ch in text.chars() {
if ch == search {
cur_count += 1;
in_search_chars = true;
} else if in_search_chars {
max_count = max_count.max(cur_count);
cur_count = 0;
in_search_chars = false;
}
}
max_count.max(cur_count)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_markdown_escaped() {
let input = r#"
# Heading
Another heading
===
Another heading variant
---
Paragraph with [link](https://example.com) and `code`, *emphasis*, and ~strikethrough~.
```
code block
```
List with varying leaders:
- Item 1
* Item 2
+ Item 3
Some math: $`\sqrt{3x-1}+(1+x)^2`$
HTML entity: &nbsp;
"#;
let expected = r#"
\# Heading
Another heading
\=\=\=
Another heading variant
\-\-\-
Paragraph with \[link](https://example.com) and \`code\`, \*emphasis\*, and \~strikethrough\~.
\`\`\`
code block
\`\`\`
List with varying leaders:
\- Item 1
\* Item 2
\+ Item 3
Some math: \$\`\\sqrt{3x\-1}\+(1\+x)\^2\`\$
HTML entity: \&nbsp;
"#;
assert_eq!(MarkdownEscaped(input).to_string(), expected);
}
#[test]
fn test_markdown_inline_code() {
assert_eq!(MarkdownInlineCode(" ").to_string(), "` `");
assert_eq!(MarkdownInlineCode("text").to_string(), "`text`");
assert_eq!(MarkdownInlineCode("text ").to_string(), "`text `");
assert_eq!(MarkdownInlineCode(" text ").to_string(), "` text `");
assert_eq!(MarkdownInlineCode("`").to_string(), "`` ` ``");
assert_eq!(MarkdownInlineCode("``").to_string(), "``` `` ```");
assert_eq!(MarkdownInlineCode("`text`").to_string(), "`` `text` ``");
assert_eq!(
MarkdownInlineCode("some `text` no leading or trailing backticks").to_string(),
"``some `text` no leading or trailing backticks``"
);
}
#[test]
fn test_count_max_consecutive_chars() {
assert_eq!(
count_max_consecutive_chars("``a```b``", '`'),
3,
"the highest seen consecutive segment of backticks counts"
);
assert_eq!(
count_max_consecutive_chars("```a``b`", '`'),
3,
"it can't be downgraded later"
);
}
}
File diff suppressed because it is too large Load Diff
-15
View File
@@ -1,15 +0,0 @@
/// Whether a given environment variable name should have its value redacted
pub fn should_redact(env_var_name: &str) -> bool {
const REDACTED_SUFFIXES: &[&str] = &[
"KEY",
"TOKEN",
"PASSWORD",
"SECRET",
"PASS",
"CREDENTIALS",
"LICENSE",
];
REDACTED_SUFFIXES
.iter()
.any(|suffix| env_var_name.ends_with(suffix))
}
-590
View File
@@ -1,590 +0,0 @@
use crate::paths::{PathStyle, is_absolute};
use anyhow::{Context as _, Result, anyhow};
use serde::{Deserialize, Serialize};
use std::{
borrow::{Borrow, Cow},
fmt,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
};
/// A file system path that is guaranteed to be relative and normalized.
///
/// This type can be used to represent paths in a uniform way, regardless of
/// whether they refer to Windows or POSIX file systems, and regardless of
/// the host platform.
///
/// Internally, paths are stored in POSIX ('/'-delimited) format, but they can
/// be displayed in either POSIX or Windows format.
///
/// Relative paths are also guaranteed to be valid unicode.
#[repr(transparent)]
#[derive(PartialEq, Eq, Hash, Serialize)]
pub struct RelPath(str);
/// An owned representation of a file system path that is guaranteed to be
/// relative and normalized.
///
/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct RelPathBuf(String);
impl RelPath {
/// Creates an empty [`RelPath`].
pub fn empty() -> &'static Self {
Self::new_unchecked("")
}
/// Converts a path with a given style into a [`RelPath`].
///
/// Returns an error if the path is absolute, or is not valid unicode.
///
/// This method will normalize the path by removing `.` components,
/// processing `..` components, and removing trailing separators. It does
/// not allocate unless it's necessary to reformat the path.
#[track_caller]
pub fn new<'a>(path: &'a Path, path_style: PathStyle) -> Result<Cow<'a, Self>> {
let mut path = path.to_str().context("non utf-8 path")?;
let (prefixes, suffixes): (&[_], &[_]) = match path_style {
PathStyle::Posix => (&["./"], &['/']),
PathStyle::Windows => (&["./", ".\\"], &['/', '\\']),
};
while prefixes.iter().any(|prefix| path.starts_with(prefix)) {
path = &path[prefixes[0].len()..];
}
while let Some(prefix) = path.strip_suffix(suffixes)
&& !prefix.is_empty()
{
path = prefix;
}
if is_absolute(&path, path_style) {
return Err(anyhow!("absolute path not allowed: {path:?}"));
}
let mut string = Cow::Borrowed(path);
if path_style == PathStyle::Windows && path.contains('\\') {
string = Cow::Owned(string.as_ref().replace('\\', "/"))
}
let mut result = match string {
Cow::Borrowed(string) => Cow::Borrowed(Self::new_unchecked(string)),
Cow::Owned(string) => Cow::Owned(RelPathBuf(string)),
};
if result
.components()
.any(|component| component == "" || component == "." || component == "..")
{
let mut normalized = RelPathBuf::new();
for component in result.components() {
match component {
"" => {}
"." => {}
".." => {
if !normalized.pop() {
return Err(anyhow!("path is not relative: {result:?}"));
}
}
other => normalized.push(RelPath::new_unchecked(other)),
}
}
result = Cow::Owned(normalized)
}
Ok(result)
}
/// Converts a path that is already normalized and uses '/' separators
/// into a [`RelPath`] .
///
/// Returns an error if the path is not already in the correct format.
#[track_caller]
pub fn unix<S: AsRef<Path> + ?Sized>(path: &S) -> anyhow::Result<&Self> {
let path = path.as_ref();
match Self::new(path, PathStyle::Posix)? {
Cow::Borrowed(path) => Ok(path),
Cow::Owned(_) => Err(anyhow!("invalid relative path {path:?}")),
}
}
fn new_unchecked(s: &str) -> &Self {
// Safety: `RelPath` is a transparent wrapper around `str`.
unsafe { &*(s as *const str as *const Self) }
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn components(&self) -> RelPathComponents<'_> {
RelPathComponents(&self.0)
}
pub fn ancestors(&self) -> RelPathAncestors<'_> {
RelPathAncestors(Some(&self.0))
}
pub fn file_name(&self) -> Option<&str> {
self.components().next_back()
}
pub fn file_stem(&self) -> Option<&str> {
Some(self.as_std_path().file_stem()?.to_str().unwrap())
}
pub fn extension(&self) -> Option<&str> {
Some(self.as_std_path().extension()?.to_str().unwrap())
}
pub fn parent(&self) -> Option<&Self> {
let mut components = self.components();
components.next_back()?;
Some(components.rest())
}
pub fn starts_with(&self, other: &Self) -> bool {
self.strip_prefix(other).is_ok()
}
pub fn ends_with(&self, other: &Self) -> bool {
if let Some(suffix) = self.0.strip_suffix(&other.0) {
if suffix.ends_with('/') {
return true;
} else if suffix.is_empty() {
return true;
}
}
false
}
pub fn strip_prefix<'a>(&'a self, other: &Self) -> Result<&'a Self, StripPrefixError> {
if other.is_empty() {
return Ok(self);
}
if let Some(suffix) = self.0.strip_prefix(&other.0) {
if let Some(suffix) = suffix.strip_prefix('/') {
return Ok(Self::new_unchecked(suffix));
} else if suffix.is_empty() {
return Ok(Self::empty());
}
}
Err(StripPrefixError)
}
pub fn len(&self) -> usize {
self.0.matches('/').count() + 1
}
pub fn last_n_components(&self, count: usize) -> Option<&Self> {
let len = self.len();
if len >= count {
let mut components = self.components();
for _ in 0..(len - count) {
components.next()?;
}
Some(components.rest())
} else {
None
}
}
pub fn join(&self, other: &Self) -> Arc<Self> {
let result = if self.0.is_empty() {
Cow::Borrowed(&other.0)
} else if other.0.is_empty() {
Cow::Borrowed(&self.0)
} else {
Cow::Owned(format!("{}/{}", &self.0, &other.0))
};
Arc::from(Self::new_unchecked(result.as_ref()))
}
pub fn to_rel_path_buf(&self) -> RelPathBuf {
RelPathBuf(self.0.to_string())
}
pub fn into_arc(&self) -> Arc<Self> {
Arc::from(self)
}
/// Convert the path into the wire representation.
pub fn to_proto(&self) -> String {
self.as_unix_str().to_owned()
}
/// Load the path from its wire representation.
pub fn from_proto(path: &str) -> Result<Arc<Self>> {
Ok(Arc::from(Self::unix(path)?))
}
/// Convert the path into a string with the given path style.
///
/// Whenever a path is presented to the user, it should be converted to
/// a string via this method.
pub fn display(&self, style: PathStyle) -> Cow<'_, str> {
match style {
PathStyle::Posix => Cow::Borrowed(&self.0),
PathStyle::Windows if self.0.contains('/') => Cow::Owned(self.0.replace('/', "\\")),
PathStyle::Windows => Cow::Borrowed(&self.0),
}
}
/// Get the internal unix-style representation of the path.
///
/// This should not be shown to the user.
pub fn as_unix_str(&self) -> &str {
&self.0
}
/// Interprets the path as a [`std::path::Path`], suitable for file system calls.
///
/// This is guaranteed to be a valid path regardless of the host platform, because
/// the `/` is accepted as a path separator on windows.
///
/// This should not be shown to the user.
pub fn as_std_path(&self) -> &Path {
Path::new(&self.0)
}
}
#[derive(Debug)]
pub struct StripPrefixError;
impl ToOwned for RelPath {
type Owned = RelPathBuf;
fn to_owned(&self) -> Self::Owned {
self.to_rel_path_buf()
}
}
impl Borrow<RelPath> for RelPathBuf {
fn borrow(&self) -> &RelPath {
self.as_rel_path()
}
}
impl PartialOrd for RelPath {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for RelPath {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.components().cmp(other.components())
}
}
impl fmt::Debug for RelPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl fmt::Debug for RelPathBuf {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
impl RelPathBuf {
pub fn new() -> Self {
Self(String::new())
}
pub fn pop(&mut self) -> bool {
if let Some(ix) = self.0.rfind('/') {
self.0.truncate(ix);
true
} else if !self.is_empty() {
self.0.clear();
true
} else {
false
}
}
pub fn push(&mut self, path: &RelPath) {
if !self.is_empty() {
self.0.push('/');
}
self.0.push_str(&path.0);
}
pub fn as_rel_path(&self) -> &RelPath {
RelPath::new_unchecked(self.0.as_str())
}
pub fn set_extension(&mut self, extension: &str) -> bool {
if let Some(filename) = self.file_name() {
let mut filename = PathBuf::from(filename);
filename.set_extension(extension);
self.pop();
self.0.push_str(filename.to_str().unwrap());
true
} else {
false
}
}
}
impl Into<Arc<RelPath>> for RelPathBuf {
fn into(self) -> Arc<RelPath> {
Arc::from(self.as_rel_path())
}
}
impl AsRef<RelPath> for RelPathBuf {
fn as_ref(&self) -> &RelPath {
self.as_rel_path()
}
}
impl AsRef<RelPath> for RelPath {
fn as_ref(&self) -> &RelPath {
self
}
}
impl Deref for RelPathBuf {
type Target = RelPath;
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl<'a> From<&'a RelPath> for Cow<'a, RelPath> {
fn from(value: &'a RelPath) -> Self {
Self::Borrowed(value)
}
}
impl From<&RelPath> for Arc<RelPath> {
fn from(rel_path: &RelPath) -> Self {
let bytes: Arc<str> = Arc::from(&rel_path.0);
unsafe { Arc::from_raw(Arc::into_raw(bytes) as *const RelPath) }
}
}
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn rel_path(path: &str) -> &RelPath {
RelPath::unix(path).unwrap()
}
impl PartialEq<str> for RelPath {
fn eq(&self, other: &str) -> bool {
self.0 == *other
}
}
#[derive(Default)]
pub struct RelPathComponents<'a>(&'a str);
pub struct RelPathAncestors<'a>(Option<&'a str>);
const SEPARATOR: char = '/';
impl<'a> RelPathComponents<'a> {
pub fn rest(&self) -> &'a RelPath {
RelPath::new_unchecked(self.0)
}
}
impl<'a> Iterator for RelPathComponents<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sep_ix) = self.0.find(SEPARATOR) {
let (head, tail) = self.0.split_at(sep_ix);
self.0 = &tail[1..];
Some(head)
} else if self.0.is_empty() {
None
} else {
let result = self.0;
self.0 = "";
Some(result)
}
}
}
impl<'a> Iterator for RelPathAncestors<'a> {
type Item = &'a RelPath;
fn next(&mut self) -> Option<Self::Item> {
let result = self.0?;
if let Some(sep_ix) = result.rfind(SEPARATOR) {
self.0 = Some(&result[..sep_ix]);
} else if !result.is_empty() {
self.0 = Some("");
} else {
self.0 = None;
}
Some(RelPath::new_unchecked(result))
}
}
impl<'a> DoubleEndedIterator for RelPathComponents<'a> {
fn next_back(&mut self) -> Option<Self::Item> {
if let Some(sep_ix) = self.0.rfind(SEPARATOR) {
let (head, tail) = self.0.split_at(sep_ix);
self.0 = head;
Some(&tail[1..])
} else if self.0.is_empty() {
None
} else {
let result = self.0;
self.0 = "";
Some(result)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use itertools::Itertools;
use pretty_assertions::assert_matches;
#[test]
fn test_rel_path_new() {
assert!(RelPath::new(Path::new("/"), PathStyle::local()).is_err());
assert!(RelPath::new(Path::new("//"), PathStyle::local()).is_err());
assert!(RelPath::new(Path::new("/foo/"), PathStyle::local()).is_err());
let path = RelPath::new("foo/".as_ref(), PathStyle::local()).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
let path = RelPath::new("foo\\".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
assert_eq!(
RelPath::new("foo/bar/../baz/./quux/".as_ref(), PathStyle::local())
.unwrap()
.as_ref(),
rel_path("foo/baz/quux")
);
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Posix).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Borrowed(_));
let path = RelPath::new(".\\foo".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
let path = RelPath::new("./.\\./foo/\\/".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path, rel_path("foo").into());
assert_matches!(path, Cow::Borrowed(_));
let path = RelPath::new("foo/./bar".as_ref(), PathStyle::Posix).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Owned(_));
let path = RelPath::new("./foo/bar".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Borrowed(_));
let path = RelPath::new(".\\foo\\bar".as_ref(), PathStyle::Windows).unwrap();
assert_eq!(path.as_ref(), rel_path("foo/bar"));
assert_matches!(path, Cow::Owned(_));
}
#[test]
fn test_rel_path_components() {
let path = rel_path("foo/bar/baz");
assert_eq!(
path.components().collect::<Vec<_>>(),
vec!["foo", "bar", "baz"]
);
assert_eq!(
path.components().rev().collect::<Vec<_>>(),
vec!["baz", "bar", "foo"]
);
let path = rel_path("");
let mut components = path.components();
assert_eq!(components.next(), None);
}
#[test]
fn test_rel_path_ancestors() {
let path = rel_path("foo/bar/baz");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next(), Some(rel_path("foo/bar/baz")));
assert_eq!(ancestors.next(), Some(rel_path("foo/bar")));
assert_eq!(ancestors.next(), Some(rel_path("foo")));
assert_eq!(ancestors.next(), Some(rel_path("")));
assert_eq!(ancestors.next(), None);
let path = rel_path("foo");
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next(), Some(rel_path("foo")));
assert_eq!(ancestors.next(), Some(RelPath::empty()));
assert_eq!(ancestors.next(), None);
let path = RelPath::empty();
let mut ancestors = path.ancestors();
assert_eq!(ancestors.next(), Some(RelPath::empty()));
assert_eq!(ancestors.next(), None);
}
#[test]
fn test_rel_path_parent() {
assert_eq!(rel_path("foo/bar/baz").parent(), Some(rel_path("foo/bar")));
assert_eq!(rel_path("foo").parent(), Some(RelPath::empty()));
assert_eq!(rel_path("").parent(), None);
}
#[test]
fn test_rel_path_partial_ord_is_compatible_with_std() {
let test_cases = ["a/b/c", "relative/path/with/dot.", "relative/path/with.dot"];
for [lhs, rhs] in test_cases.iter().array_combinations::<2>() {
assert_eq!(
Path::new(lhs).cmp(Path::new(rhs)),
RelPath::unix(lhs)
.unwrap()
.cmp(&RelPath::unix(rhs).unwrap())
);
}
}
#[test]
fn test_strip_prefix() {
let parent = rel_path("");
let child = rel_path(".foo");
assert!(child.starts_with(parent));
assert_eq!(child.strip_prefix(parent).unwrap(), child);
}
#[test]
fn test_rel_path_constructors_absolute_path() {
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("\\a\\b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("/a/b"), PathStyle::Posix).is_err());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("C:\\a\\b"), PathStyle::Windows).is_err());
assert!(RelPath::new(Path::new("C:/a/b"), PathStyle::Posix).is_ok());
}
#[test]
fn test_pop() {
let mut path = rel_path("a/b").to_rel_path_buf();
path.pop();
assert_eq!(path.as_rel_path().as_unix_str(), "a");
path.pop();
assert_eq!(path.as_rel_path().as_unix_str(), "");
path.pop();
assert_eq!(path.as_rel_path().as_unix_str(), "");
}
}
-72
View File
@@ -1,72 +0,0 @@
use schemars::{JsonSchema, transform::transform_subschemas};
const DEFS_PATH: &str = "#/$defs/";
/// Replaces the JSON schema definition for some type if it is in use (in the definitions list), and
/// returns a reference to it.
///
/// This asserts that JsonSchema::schema_name() + "2" does not exist because this indicates that
/// there are multiple types that use this name, and unfortunately schemars APIs do not support
/// resolving this ambiguity - see <https://github.com/GREsau/schemars/issues/449>
///
/// This takes a closure for `schema` because some settings types are not available on the remote
/// server, and so will crash when attempting to access e.g. GlobalThemeRegistry.
pub fn replace_subschema<T: JsonSchema>(
generator: &mut schemars::SchemaGenerator,
schema: impl Fn() -> schemars::Schema,
) -> schemars::Schema {
let schema_name = T::schema_name();
let definitions = generator.definitions_mut();
assert!(!definitions.contains_key(&format!("{schema_name}2")));
assert!(definitions.contains_key(schema_name.as_ref()));
definitions.insert(schema_name.to_string(), schema().to_value());
schemars::Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
}
/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
/// already in use.
pub fn add_new_subschema(
generator: &mut schemars::SchemaGenerator,
name: &str,
schema: serde_json::Value,
) -> schemars::Schema {
let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
assert_eq!(old_definition, None);
schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
}
/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used
/// on a map).
#[derive(Clone)]
pub struct DefaultDenyUnknownFields;
impl schemars::transform::Transform for DefaultDenyUnknownFields {
fn transform(&mut self, schema: &mut schemars::Schema) {
if let Some(object) = schema.as_object_mut()
&& object.contains_key("properties")
&& !object.contains_key("additionalProperties")
&& !object.contains_key("unevaluatedProperties")
{
object.insert("additionalProperties".to_string(), false.into());
}
transform_subschemas(self, schema);
}
}
/// Defaults `allowTrailingCommas` to `true`, for use with `json-language-server`.
/// This can be applied to any schema that will be treated as `jsonc`.
///
/// Note that this is non-recursive and only applied to the root schema.
#[derive(Clone)]
pub struct AllowTrailingCommas;
impl schemars::transform::Transform for AllowTrailingCommas {
fn transform(&mut self, schema: &mut schemars::Schema) {
if let Some(object) = schema.as_object_mut()
&& !object.contains_key("allowTrailingCommas")
{
object.insert("allowTrailingCommas".to_string(), true.into());
}
}
}
-7
View File
@@ -1,7 +0,0 @@
pub const fn default_true() -> bool {
true
}
pub fn is_default<T: Default + PartialEq>(value: &T) -> bool {
*value == T::default()
}
-980
View File
@@ -1,980 +0,0 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{borrow::Cow, fmt, path::Path, sync::LazyLock};
/// Shell configuration to open the terminal with.
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Shell {
/// Use the system's default terminal configuration in /etc/passwd
#[default]
System,
/// Use a specific program with no arguments.
Program(String),
/// Use a specific program with arguments.
WithArguments {
/// The program to run.
program: String,
/// The arguments to pass to the program.
args: Vec<String>,
/// An optional string to override the title of the terminal tab
title_override: Option<String>,
},
}
impl Shell {
pub fn program(&self) -> String {
match self {
Shell::Program(program) => program.clone(),
Shell::WithArguments { program, .. } => program.clone(),
Shell::System => get_system_shell(),
}
}
pub fn program_and_args(&self) -> (String, &[String]) {
match self {
Shell::Program(program) => (program.clone(), &[]),
Shell::WithArguments { program, args, .. } => (program.clone(), args),
Shell::System => (get_system_shell(), &[]),
}
}
pub fn shell_kind(&self, is_windows: bool) -> ShellKind {
match self {
Shell::Program(program) => ShellKind::new(program, is_windows),
Shell::WithArguments { program, .. } => ShellKind::new(program, is_windows),
Shell::System => ShellKind::system(),
}
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ShellKind {
#[default]
Posix,
Csh,
Tcsh,
Rc,
Fish,
/// Pre-installed "legacy" powershell for windows
PowerShell,
/// PowerShell 7.x
Pwsh,
Nushell,
Cmd,
Xonsh,
Elvish,
}
pub fn get_system_shell() -> String {
if cfg!(windows) {
get_windows_system_shell()
} else {
std::env::var("SHELL").unwrap_or("/bin/sh".to_string())
}
}
pub fn get_default_system_shell() -> String {
if cfg!(windows) {
get_windows_system_shell()
} else {
"/bin/sh".to_string()
}
}
/// Get the default system shell, preferring bash on Windows.
pub fn get_default_system_shell_preferring_bash() -> String {
if cfg!(windows) {
get_windows_bash().unwrap_or_else(|| get_windows_system_shell())
} else {
"/bin/sh".to_string()
}
}
pub fn get_windows_bash() -> Option<String> {
use std::path::PathBuf;
fn find_bash_in_scoop() -> Option<PathBuf> {
let bash_exe =
PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\bash.exe");
bash_exe.exists().then_some(bash_exe)
}
fn find_bash_in_git() -> Option<PathBuf> {
// /path/to/git/cmd/git.exe/../../bin/bash.exe
let git = which::which("git").ok()?;
let git_bash = git.parent()?.parent()?.join("bin").join("bash.exe");
git_bash.exists().then_some(git_bash)
}
static BASH: LazyLock<Option<String>> = LazyLock::new(|| {
let bash = find_bash_in_scoop()
.or_else(|| find_bash_in_git())
.map(|p| p.to_string_lossy().into_owned());
if let Some(ref path) = bash {
log::info!("Found bash at {}", path);
}
bash
});
(*BASH).clone()
}
pub fn get_windows_system_shell() -> String {
use std::path::PathBuf;
fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
#[cfg(target_pointer_width = "64")]
let env_var = if find_alternate {
"ProgramFiles(x86)"
} else {
"ProgramFiles"
};
#[cfg(target_pointer_width = "32")]
let env_var = if find_alternate {
"ProgramW6432"
} else {
"ProgramFiles"
};
let install_base_dir = PathBuf::from(std::env::var_os(env_var)?).join("PowerShell");
install_base_dir
.read_dir()
.ok()?
.filter_map(Result::ok)
.filter(|entry| matches!(entry.file_type(), Ok(ft) if ft.is_dir()))
.filter_map(|entry| {
let dir_name = entry.file_name();
let dir_name = dir_name.to_string_lossy();
let version = if find_preview {
let dash_index = dir_name.find('-')?;
if &dir_name[dash_index + 1..] != "preview" {
return None;
};
dir_name[..dash_index].parse::<u32>().ok()?
} else {
dir_name.parse::<u32>().ok()?
};
let exe_path = entry.path().join("pwsh.exe");
if exe_path.exists() {
Some((version, exe_path))
} else {
None
}
})
.max_by_key(|(version, _)| *version)
.map(|(_, path)| path)
}
fn find_pwsh_in_msix(find_preview: bool) -> Option<PathBuf> {
let msix_app_dir =
PathBuf::from(std::env::var_os("LOCALAPPDATA")?).join("Microsoft\\WindowsApps");
if !msix_app_dir.exists() {
return None;
}
let prefix = if find_preview {
"Microsoft.PowerShellPreview_"
} else {
"Microsoft.PowerShell_"
};
msix_app_dir
.read_dir()
.ok()?
.filter_map(|entry| {
let entry = entry.ok()?;
if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
return None;
}
if !entry.file_name().to_string_lossy().starts_with(prefix) {
return None;
}
let exe_path = entry.path().join("pwsh.exe");
exe_path.exists().then_some(exe_path)
})
.next()
}
fn find_pwsh_in_scoop() -> Option<PathBuf> {
let pwsh_exe =
PathBuf::from(std::env::var_os("USERPROFILE")?).join("scoop\\shims\\pwsh.exe");
pwsh_exe.exists().then_some(pwsh_exe)
}
static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
let locations = [
|| find_pwsh_in_programfiles(false, false),
|| find_pwsh_in_programfiles(true, false),
|| find_pwsh_in_msix(false),
|| find_pwsh_in_programfiles(false, true),
|| find_pwsh_in_msix(true),
|| find_pwsh_in_programfiles(true, true),
|| find_pwsh_in_scoop(),
|| which::which_global("pwsh.exe").ok(),
|| which::which_global("powershell.exe").ok(),
];
locations
.into_iter()
.find_map(|f| f())
.map(|p| p.to_string_lossy().trim().to_owned())
.inspect(|shell| log::info!("Found powershell in: {}", shell))
.unwrap_or_else(|| {
log::warn!("Powershell not found, falling back to `cmd`");
"cmd.exe".to_string()
})
});
(*SYSTEM_SHELL).clone()
}
impl fmt::Display for ShellKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ShellKind::Posix => write!(f, "sh"),
ShellKind::Csh => write!(f, "csh"),
ShellKind::Tcsh => write!(f, "tcsh"),
ShellKind::Fish => write!(f, "fish"),
ShellKind::PowerShell => write!(f, "powershell"),
ShellKind::Pwsh => write!(f, "pwsh"),
ShellKind::Nushell => write!(f, "nu"),
ShellKind::Cmd => write!(f, "cmd"),
ShellKind::Rc => write!(f, "rc"),
ShellKind::Xonsh => write!(f, "xonsh"),
ShellKind::Elvish => write!(f, "elvish"),
}
}
}
impl ShellKind {
pub fn system() -> Self {
Self::new(&get_system_shell(), cfg!(windows))
}
pub fn new(program: impl AsRef<Path>, is_windows: bool) -> Self {
let program = program.as_ref();
let program = program
.file_stem()
.unwrap_or_else(|| program.as_os_str())
.to_string_lossy();
match &*program {
"powershell" => ShellKind::PowerShell,
"pwsh" => ShellKind::Pwsh,
"cmd" => ShellKind::Cmd,
"nu" => ShellKind::Nushell,
"fish" => ShellKind::Fish,
"csh" => ShellKind::Csh,
"tcsh" => ShellKind::Tcsh,
"rc" => ShellKind::Rc,
"xonsh" => ShellKind::Xonsh,
"elvish" => ShellKind::Elvish,
"sh" | "bash" | "zsh" => ShellKind::Posix,
_ if is_windows => ShellKind::PowerShell,
// Some other shell detected, the user might install and use a
// unix-like shell.
_ => ShellKind::Posix,
}
}
pub fn to_shell_variable(self, input: &str) -> String {
match self {
Self::PowerShell | Self::Pwsh => Self::to_powershell_variable(input),
Self::Cmd => Self::to_cmd_variable(input),
Self::Posix => input.to_owned(),
Self::Fish => input.to_owned(),
Self::Csh => input.to_owned(),
Self::Tcsh => input.to_owned(),
Self::Rc => input.to_owned(),
Self::Nushell => Self::to_nushell_variable(input),
Self::Xonsh => input.to_owned(),
Self::Elvish => input.to_owned(),
}
}
fn to_cmd_variable(input: &str) -> String {
if let Some(var_str) = input.strip_prefix("${") {
if var_str.find(':').is_none() {
// If the input starts with "${", remove the trailing "}"
format!("%{}%", &var_str[..var_str.len() - 1])
} else {
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
// which will result in the task failing to run in such cases.
input.into()
}
} else if let Some(var_str) = input.strip_prefix('$') {
// If the input starts with "$", directly append to "$env:"
format!("%{}%", var_str)
} else {
// If no prefix is found, return the input as is
input.into()
}
}
fn to_powershell_variable(input: &str) -> String {
if let Some(var_str) = input.strip_prefix("${") {
if var_str.find(':').is_none() {
// If the input starts with "${", remove the trailing "}"
format!("$env:{}", &var_str[..var_str.len() - 1])
} else {
// `${SOME_VAR:-SOME_DEFAULT}`, we currently do not handle this situation,
// which will result in the task failing to run in such cases.
input.into()
}
} else if let Some(var_str) = input.strip_prefix('$') {
// If the input starts with "$", directly append to "$env:"
format!("$env:{}", var_str)
} else {
// If no prefix is found, return the input as is
input.into()
}
}
fn to_nushell_variable(input: &str) -> String {
let mut result = String::new();
let mut source = input;
let mut is_start = true;
loop {
match source.chars().next() {
None => return result,
Some('$') => {
source = Self::parse_nushell_var(&source[1..], &mut result, is_start);
is_start = false;
}
Some(_) => {
is_start = false;
let chunk_end = source.find('$').unwrap_or(source.len());
let (chunk, rest) = source.split_at(chunk_end);
result.push_str(chunk);
source = rest;
}
}
}
}
fn parse_nushell_var<'a>(source: &'a str, text: &mut String, is_start: bool) -> &'a str {
if source.starts_with("env.") {
text.push('$');
return source;
}
match source.chars().next() {
Some('{') => {
let source = &source[1..];
if let Some(end) = source.find('}') {
let var_name = &source[..end];
if !var_name.is_empty() {
if !is_start {
text.push_str("(");
}
text.push_str("$env.");
text.push_str(var_name);
if !is_start {
text.push_str(")");
}
&source[end + 1..]
} else {
text.push_str("${}");
&source[end + 1..]
}
} else {
text.push_str("${");
source
}
}
Some(c) if c.is_alphabetic() || c == '_' => {
let end = source
.find(|c: char| !c.is_alphanumeric() && c != '_')
.unwrap_or(source.len());
let var_name = &source[..end];
if !is_start {
text.push_str("(");
}
text.push_str("$env.");
text.push_str(var_name);
if !is_start {
text.push_str(")");
}
&source[end..]
}
_ => {
text.push('$');
source
}
}
}
pub fn args_for_shell(&self, interactive: bool, combined_command: String) -> Vec<String> {
match self {
ShellKind::PowerShell | ShellKind::Pwsh => vec!["-C".to_owned(), combined_command],
ShellKind::Cmd => vec![
"/S".to_owned(),
"/C".to_owned(),
format!("\"{combined_command}\""),
],
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Fish
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => interactive
.then(|| "-i".to_owned())
.into_iter()
.chain(["-c".to_owned(), combined_command])
.collect(),
}
}
pub const fn command_prefix(&self) -> Option<char> {
match self {
ShellKind::PowerShell | ShellKind::Pwsh => Some('&'),
ShellKind::Nushell => Some('^'),
ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::Cmd
| ShellKind::Xonsh
| ShellKind::Elvish => None,
}
}
pub fn prepend_command_prefix<'a>(&self, command: &'a str) -> Cow<'a, str> {
match self.command_prefix() {
Some(prefix) if !command.starts_with(prefix) => {
Cow::Owned(format!("{prefix}{command}"))
}
_ => Cow::Borrowed(command),
}
}
pub const fn sequential_commands_separator(&self) -> char {
match self {
ShellKind::Cmd => '&',
ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::PowerShell
| ShellKind::Pwsh
| ShellKind::Nushell
| ShellKind::Xonsh
| ShellKind::Elvish => ';',
}
}
pub const fn sequential_and_commands_separator(&self) -> &'static str {
match self {
ShellKind::Cmd
| ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::Pwsh
| ShellKind::PowerShell
| ShellKind::Xonsh => "&&",
ShellKind::Nushell | ShellKind::Elvish => ";",
}
}
pub fn try_quote<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
match self {
ShellKind::PowerShell => Some(Self::quote_powershell(arg)),
ShellKind::Pwsh => Some(Self::quote_pwsh(arg)),
ShellKind::Cmd => Some(Self::quote_cmd(arg)),
ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::Nushell
| ShellKind::Xonsh
| ShellKind::Elvish => shlex::try_quote(arg).ok(),
}
}
fn quote_windows(arg: &str, enclose: bool) -> Cow<'_, str> {
if arg.is_empty() {
return Cow::Borrowed("\"\"");
}
let needs_quoting = arg.chars().any(|c| c == ' ' || c == '\t' || c == '"');
if !needs_quoting {
return Cow::Borrowed(arg);
}
let mut result = String::with_capacity(arg.len() + 2);
if enclose {
result.push('"');
}
let chars: Vec<char> = arg.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '\\' {
let mut num_backslashes = 0;
while i < chars.len() && chars[i] == '\\' {
num_backslashes += 1;
i += 1;
}
if i < chars.len() && chars[i] == '"' {
// Backslashes followed by quote: double the backslashes and escape the quote
for _ in 0..(num_backslashes * 2 + 1) {
result.push('\\');
}
result.push('"');
i += 1;
} else if i >= chars.len() {
// Trailing backslashes: double them (they precede the closing quote)
for _ in 0..(num_backslashes * 2) {
result.push('\\');
}
} else {
// Backslashes not followed by quote: output as-is
for _ in 0..num_backslashes {
result.push('\\');
}
}
} else if chars[i] == '"' {
// Quote not preceded by backslash: escape it
result.push('\\');
result.push('"');
i += 1;
} else {
result.push(chars[i]);
i += 1;
}
}
if enclose {
result.push('"');
}
Cow::Owned(result)
}
fn needs_quoting_powershell(s: &str) -> bool {
s.is_empty()
|| s.chars().any(|c| {
c.is_whitespace()
|| matches!(
c,
'"' | '`'
| '$'
| '&'
| '|'
| '<'
| '>'
| ';'
| '('
| ')'
| '['
| ']'
| '{'
| '}'
| ','
| '\''
| '@'
)
})
}
fn need_quotes_powershell(arg: &str) -> bool {
let mut quote_count = 0;
for c in arg.chars() {
if c == '"' {
quote_count += 1;
} else if c.is_whitespace() && (quote_count % 2 == 0) {
return true;
}
}
false
}
fn escape_powershell_quotes(s: &str) -> String {
let mut result = String::with_capacity(s.len() + 4);
result.push('\'');
for c in s.chars() {
if c == '\'' {
result.push('\'');
}
result.push(c);
}
result.push('\'');
result
}
pub fn quote_powershell(arg: &str) -> Cow<'_, str> {
let ps_will_quote = Self::need_quotes_powershell(arg);
let crt_quoted = Self::quote_windows(arg, !ps_will_quote);
if !Self::needs_quoting_powershell(arg) {
return crt_quoted;
}
Cow::Owned(Self::escape_powershell_quotes(&crt_quoted))
}
pub fn quote_pwsh(arg: &str) -> Cow<'_, str> {
if arg.is_empty() {
return Cow::Borrowed("''");
}
if !Self::needs_quoting_powershell(arg) {
return Cow::Borrowed(arg);
}
Cow::Owned(Self::escape_powershell_quotes(arg))
}
pub fn quote_cmd(arg: &str) -> Cow<'_, str> {
let crt_quoted = Self::quote_windows(arg, true);
let needs_cmd_escaping = crt_quoted.contains('"')
|| crt_quoted.contains('%')
|| crt_quoted
.chars()
.any(|c| matches!(c, '^' | '<' | '>' | '&' | '|' | '(' | ')'));
if !needs_cmd_escaping {
return crt_quoted;
}
let mut result = String::with_capacity(crt_quoted.len() * 2);
for c in crt_quoted.chars() {
match c {
'^' | '"' | '<' | '>' | '&' | '|' | '(' | ')' => {
result.push('^');
result.push(c);
}
'%' => {
result.push_str("%%cd:~,%");
}
_ => result.push(c),
}
}
Cow::Owned(result)
}
/// Quotes the given argument if necessary, taking into account the command prefix.
///
/// In other words, this will consider quoting arg without its command prefix to not break the command.
/// You should use this over `try_quote` when you want to quote a shell command.
pub fn try_quote_prefix_aware<'a>(&self, arg: &'a str) -> Option<Cow<'a, str>> {
if let Some(char) = self.command_prefix() {
if let Some(arg) = arg.strip_prefix(char) {
// we have a command that is prefixed
for quote in ['\'', '"'] {
if let Some(arg) = arg
.strip_prefix(quote)
.and_then(|arg| arg.strip_suffix(quote))
{
// and the command itself is wrapped as a literal, that
// means the prefix exists to interpret a literal as a
// command. So strip the quotes, quote the command, and
// re-add the quotes if they are missing after requoting
let quoted = self.try_quote(arg)?;
return Some(if quoted.starts_with(['\'', '"']) {
Cow::Owned(self.prepend_command_prefix(&quoted).into_owned())
} else {
Cow::Owned(
self.prepend_command_prefix(&format!("{quote}{quoted}{quote}"))
.into_owned(),
)
});
}
}
return self
.try_quote(arg)
.map(|quoted| Cow::Owned(self.prepend_command_prefix(&quoted).into_owned()));
}
}
self.try_quote(arg).map(|quoted| match quoted {
unquoted @ Cow::Borrowed(_) => unquoted,
Cow::Owned(quoted) => Cow::Owned(self.prepend_command_prefix(&quoted).into_owned()),
})
}
pub fn split(&self, input: &str) -> Option<Vec<String>> {
shlex::split(input)
}
pub const fn activate_keyword(&self) -> &'static str {
match self {
ShellKind::Cmd => "",
ShellKind::Nushell => "overlay use",
ShellKind::PowerShell | ShellKind::Pwsh => ".",
ShellKind::Fish
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Posix
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => "source",
}
}
pub const fn clear_screen_command(&self) -> &'static str {
match self {
ShellKind::Cmd => "cls",
ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::PowerShell
| ShellKind::Pwsh
| ShellKind::Nushell
| ShellKind::Xonsh
| ShellKind::Elvish => "clear",
}
}
#[cfg(windows)]
/// We do not want to escape arguments if we are using CMD as our shell.
/// If we do we end up with too many quotes/escaped quotes for CMD to handle.
pub const fn tty_escape_args(&self) -> bool {
match self {
ShellKind::Cmd => false,
ShellKind::Posix
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Fish
| ShellKind::PowerShell
| ShellKind::Pwsh
| ShellKind::Nushell
| ShellKind::Xonsh
| ShellKind::Elvish => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// Examples
// WSL
// wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "echo hello"
// wsl.exe --distribution NixOS --cd /home/user -- /usr/bin/zsh -c "\"echo hello\"" | grep hello"
// wsl.exe --distribution NixOS --cd ~ env RUST_LOG=info,remote=debug .zed_wsl_server/zed-remote-server-dev-build proxy --identifier dev-workspace-53
// PowerShell from Nushell
// nu -c overlay use "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\activate.nu"; ^"C:\Program Files\PowerShell\7\pwsh.exe" -C "C:\Users\kubko\dev\python\39007\tests\.venv\Scripts\python.exe -m pytest \"test_foo.py::test_foo\""
// PowerShell from CMD
// cmd /C \" \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\activate.bat\"& \"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe\" -C \"C:\\\\Users\\\\kubko\\\\dev\\\\python\\\\39007\\\\tests\\\\.venv\\\\Scripts\\\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"\"\"
#[test]
fn test_try_quote_powershell() {
let shell_kind = ShellKind::PowerShell;
assert_eq!(
shell_kind
.try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
.unwrap()
.into_owned(),
"'C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\\"test_foo.py::test_foo\\\"'".to_string()
);
}
#[test]
fn test_try_quote_cmd() {
let shell_kind = ShellKind::Cmd;
assert_eq!(
shell_kind
.try_quote("C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \"test_foo.py::test_foo\"")
.unwrap()
.into_owned(),
"^\"C:\\Users\\johndoe\\dev\\python\\39007\\tests\\.venv\\Scripts\\python.exe -m pytest \\^\"test_foo.py::test_foo\\^\"^\"".to_string()
);
}
#[test]
fn test_try_quote_powershell_edge_cases() {
let shell_kind = ShellKind::PowerShell;
// Empty string
assert_eq!(
shell_kind.try_quote("").unwrap().into_owned(),
"'\"\"'".to_string()
);
// String without special characters (no quoting needed)
assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple");
// String with spaces
assert_eq!(
shell_kind.try_quote("hello world").unwrap().into_owned(),
"'hello world'".to_string()
);
// String with dollar signs
assert_eq!(
shell_kind.try_quote("$variable").unwrap().into_owned(),
"'$variable'".to_string()
);
// String with backticks
assert_eq!(
shell_kind.try_quote("test`command").unwrap().into_owned(),
"'test`command'".to_string()
);
// String with multiple special characters
assert_eq!(
shell_kind
.try_quote("test `\"$var`\" end")
.unwrap()
.into_owned(),
"'test `\\\"$var`\\\" end'".to_string()
);
// String with backslashes and colon (path without spaces doesn't need quoting)
assert_eq!(
shell_kind.try_quote("C:\\path\\to\\file").unwrap(),
"C:\\path\\to\\file"
);
}
#[test]
fn test_try_quote_cmd_edge_cases() {
let shell_kind = ShellKind::Cmd;
// Empty string
assert_eq!(
shell_kind.try_quote("").unwrap().into_owned(),
"^\"^\"".to_string()
);
// String without special characters (no quoting needed)
assert_eq!(shell_kind.try_quote("simple").unwrap(), "simple");
// String with spaces
assert_eq!(
shell_kind.try_quote("hello world").unwrap().into_owned(),
"^\"hello world^\"".to_string()
);
// String with space and backslash (backslash not at end, so not doubled)
assert_eq!(
shell_kind.try_quote("path\\ test").unwrap().into_owned(),
"^\"path\\ test^\"".to_string()
);
// String ending with backslash (must be doubled before closing quote)
assert_eq!(
shell_kind.try_quote("test path\\").unwrap().into_owned(),
"^\"test path\\\\^\"".to_string()
);
// String ending with multiple backslashes (all doubled before closing quote)
assert_eq!(
shell_kind.try_quote("test path\\\\").unwrap().into_owned(),
"^\"test path\\\\\\\\^\"".to_string()
);
// String with embedded quote (quote is escaped, backslash before it is doubled)
assert_eq!(
shell_kind.try_quote("test\\\"quote").unwrap().into_owned(),
"^\"test\\\\\\^\"quote^\"".to_string()
);
// String with multiple backslashes before embedded quote (all doubled)
assert_eq!(
shell_kind
.try_quote("test\\\\\"quote")
.unwrap()
.into_owned(),
"^\"test\\\\\\\\\\^\"quote^\"".to_string()
);
// String with backslashes not before quotes (path without spaces doesn't need quoting)
assert_eq!(
shell_kind.try_quote("C:\\path\\to\\file").unwrap(),
"C:\\path\\to\\file"
);
}
#[test]
fn test_try_quote_nu_command() {
let shell_kind = ShellKind::Nushell;
assert_eq!(
shell_kind.try_quote("'uname'").unwrap().into_owned(),
"\"'uname'\"".to_string()
);
assert_eq!(
shell_kind
.try_quote_prefix_aware("'uname'")
.unwrap()
.into_owned(),
"^\"'uname'\"".to_string()
);
assert_eq!(
shell_kind.try_quote("^uname").unwrap().into_owned(),
"'^uname'".to_string()
);
assert_eq!(
shell_kind
.try_quote_prefix_aware("^uname")
.unwrap()
.into_owned(),
"^uname".to_string()
);
assert_eq!(
shell_kind.try_quote("^'uname'").unwrap().into_owned(),
"'^'\"'uname\'\"".to_string()
);
assert_eq!(
shell_kind
.try_quote_prefix_aware("^'uname'")
.unwrap()
.into_owned(),
"^'uname'".to_string()
);
assert_eq!(
shell_kind.try_quote("'uname a'").unwrap().into_owned(),
"\"'uname a'\"".to_string()
);
assert_eq!(
shell_kind
.try_quote_prefix_aware("'uname a'")
.unwrap()
.into_owned(),
"^\"'uname a'\"".to_string()
);
assert_eq!(
shell_kind.try_quote("^'uname a'").unwrap().into_owned(),
"'^'\"'uname a'\"".to_string()
);
assert_eq!(
shell_kind
.try_quote_prefix_aware("^'uname a'")
.unwrap()
.into_owned(),
"^'uname a'".to_string()
);
assert_eq!(
shell_kind.try_quote("uname").unwrap().into_owned(),
"uname".to_string()
);
assert_eq!(
shell_kind
.try_quote_prefix_aware("uname")
.unwrap()
.into_owned(),
"uname".to_string()
);
}
}
-298
View File
@@ -1,298 +0,0 @@
use std::borrow::Cow;
use crate::shell::get_system_shell;
use crate::shell::{Shell, ShellKind};
/// ShellBuilder is used to turn a user-requested task into a
/// program that can be executed by the shell.
pub struct ShellBuilder {
/// The shell to run
program: String,
args: Vec<String>,
interactive: bool,
/// Whether to redirect stdin to /dev/null for the spawned command as a subshell.
redirect_stdin: bool,
kind: ShellKind,
}
impl ShellBuilder {
/// Create a new ShellBuilder as configured.
pub fn new(shell: &Shell, is_windows: bool) -> Self {
let (program, args) = match shell {
Shell::System => (get_system_shell(), Vec::new()),
Shell::Program(shell) => (shell.clone(), Vec::new()),
Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()),
};
let kind = ShellKind::new(&program, is_windows);
Self {
program,
args,
interactive: true,
kind,
redirect_stdin: false,
}
}
pub fn non_interactive(mut self) -> Self {
self.interactive = false;
self
}
/// Returns the label to show in the terminal tab
pub fn command_label(&self, command_to_use_in_label: &str) -> String {
if command_to_use_in_label.trim().is_empty() {
self.program.clone()
} else {
match self.kind {
ShellKind::PowerShell | ShellKind::Pwsh => {
format!("{} -C '{}'", self.program, command_to_use_in_label)
}
ShellKind::Cmd => {
format!("{} /C \"{}\"", self.program, command_to_use_in_label)
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Fish
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
let interactivity = self.interactive.then_some("-i ").unwrap_or_default();
format!(
"{PROGRAM} {interactivity}-c '{command_to_use_in_label}'",
PROGRAM = self.program
)
}
}
}
}
pub fn redirect_stdin_to_dev_null(mut self) -> Self {
self.redirect_stdin = true;
self
}
/// Returns the program and arguments to run this task in a shell.
pub fn build(
mut self,
task_command: Option<String>,
task_args: &[String],
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let task_command = if !task_args.is_empty() {
match self.kind.try_quote_prefix_aware(&task_command) {
Some(task_command) => task_command.into_owned(),
None => task_command,
}
} else {
task_command
};
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
let shell_variable = self.kind.to_shell_variable(arg);
command.push_str(&match self.kind.try_quote(&shell_variable) {
Some(shell_variable) => shell_variable,
None => Cow::Owned(shell_variable),
});
command
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
combined_command.insert(0, '(');
combined_command.push_str(") </dev/null");
}
ShellKind::PowerShell | ShellKind::Pwsh => {
combined_command.insert_str(0, "$null | & {");
combined_command.push_str("}");
}
ShellKind::Cmd => {
combined_command.push_str("< NUL");
}
}
}
self.args
.extend(self.kind.args_for_shell(self.interactive, combined_command));
}
(self.program, self.args)
}
// This should not exist, but our task infra is broken beyond repair right now
#[doc(hidden)]
pub fn build_no_quote(
mut self,
task_command: Option<String>,
task_args: &[String],
) -> (String, Vec<String>) {
if let Some(task_command) = task_command {
let mut combined_command = task_args.iter().fold(task_command, |mut command, arg| {
command.push(' ');
command.push_str(&self.kind.to_shell_variable(arg));
command
});
if self.redirect_stdin {
match self.kind {
ShellKind::Fish => {
combined_command.insert_str(0, "begin; ");
combined_command.push_str("; end </dev/null");
}
ShellKind::Posix
| ShellKind::Nushell
| ShellKind::Csh
| ShellKind::Tcsh
| ShellKind::Rc
| ShellKind::Xonsh
| ShellKind::Elvish => {
combined_command.insert(0, '(');
combined_command.push_str(") </dev/null");
}
ShellKind::PowerShell | ShellKind::Pwsh => {
combined_command.insert_str(0, "$null | & {");
combined_command.push_str("}");
}
ShellKind::Cmd => {
combined_command.push_str("< NUL");
}
}
}
self.args
.extend(self.kind.args_for_shell(self.interactive, combined_command));
}
(self.program, self.args)
}
/// Builds a command with the given task command and arguments.
///
/// Prefer this over manually constructing a command with the output of `Self::build`,
/// as this method handles `cmd` weirdness on windows correctly.
pub fn build_command(
self,
mut task_command: Option<String>,
task_args: &[String],
) -> smol::process::Command {
#[cfg(windows)]
let kind = self.kind;
if task_args.is_empty() {
task_command = task_command
.as_ref()
.map(|cmd| self.kind.try_quote_prefix_aware(&cmd).map(Cow::into_owned))
.unwrap_or(task_command);
}
let (program, args) = self.build(task_command, task_args);
let mut child = crate::command::new_smol_command(program);
#[cfg(windows)]
if kind == ShellKind::Cmd {
use smol::process::windows::CommandExt;
for arg in args {
child.raw_arg(arg);
}
} else {
child.args(args);
}
#[cfg(not(windows))]
child.args(args);
child
}
pub fn kind(&self) -> ShellKind {
self.kind
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_nu_shell_variable_substitution() {
let shell = Shell::Program("nu".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(
Some("echo".into()),
&[
"${hello}".to_string(),
"$world".to_string(),
"nothing".to_string(),
"--$something".to_string(),
"$".to_string(),
"${test".to_string(),
],
);
assert_eq!(program, "nu");
assert_eq!(
args,
vec![
"-i",
"-c",
"echo '$env.hello' '$env.world' nothing '--($env.something)' '$' '${test'"
]
);
}
#[test]
fn redirect_stdin_to_dev_null_precedence() {
let shell = Shell::Program("nu".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some("echo".into()), &["nothing".to_string()]);
assert_eq!(program, "nu");
assert_eq!(args, vec!["-i", "-c", "(echo nothing) </dev/null"]);
}
#[test]
fn redirect_stdin_to_dev_null_fish() {
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder
.redirect_stdin_to_dev_null()
.build(Some("echo".into()), &["test".to_string()]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "begin; echo test; end </dev/null"]);
}
#[test]
fn does_not_quote_sole_command_only() {
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(Some("echo".into()), &[]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "echo"]);
let shell = Shell::Program("fish".to_owned());
let shell_builder = ShellBuilder::new(&shell, false);
let (program, args) = shell_builder.build(Some("echo oo".into()), &[]);
assert_eq!(program, "fish");
assert_eq!(args, vec!["-i", "-c", "echo oo"]);
}
}
-46
View File
@@ -1,46 +0,0 @@
pub fn format_file_size(size: u64, use_decimal: bool) -> String {
if use_decimal {
if size < 1000 {
format!("{size}B")
} else if size < 1000 * 1000 {
format!("{:.1}KB", size as f64 / 1000.0)
} else {
format!("{:.1}MB", size as f64 / (1000.0 * 1000.0))
}
} else if size < 1024 {
format!("{size}B")
} else if size < 1024 * 1024 {
format!("{:.1}KiB", size as f64 / 1024.0)
} else {
format!("{:.1}MiB", size as f64 / (1024.0 * 1024.0))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_file_size_decimal() {
assert_eq!(format_file_size(0, true), "0B");
assert_eq!(format_file_size(999, true), "999B");
assert_eq!(format_file_size(1000, true), "1.0KB");
assert_eq!(format_file_size(1500, true), "1.5KB");
assert_eq!(format_file_size(999999, true), "1000.0KB");
assert_eq!(format_file_size(1000000, true), "1.0MB");
assert_eq!(format_file_size(1500000, true), "1.5MB");
assert_eq!(format_file_size(10000000, true), "10.0MB");
}
#[test]
fn test_format_file_size_binary() {
assert_eq!(format_file_size(0, false), "0B");
assert_eq!(format_file_size(1023, false), "1023B");
assert_eq!(format_file_size(1024, false), "1.0KiB");
assert_eq!(format_file_size(1536, false), "1.5KiB");
assert_eq!(format_file_size(1048575, false), "1024.0KiB");
assert_eq!(format_file_size(1048576, false), "1.0MiB");
assert_eq!(format_file_size(1572864, false), "1.5MiB");
assert_eq!(format_file_size(10485760, false), "10.0MiB");
}
}
-81
View File
@@ -1,81 +0,0 @@
mod assertions;
mod marked_text;
use git2;
use std::{
ffi::OsStr,
path::{Path, PathBuf},
};
use tempfile::TempDir;
pub use assertions::*;
pub use marked_text::*;
pub struct TempTree {
_temp_dir: TempDir,
path: PathBuf,
}
impl TempTree {
pub fn new(tree: serde_json::Value) -> Self {
let dir = TempDir::new().unwrap();
let path = std::fs::canonicalize(dir.path()).unwrap();
write_tree(path.as_path(), tree);
Self {
_temp_dir: dir,
path,
}
}
pub fn path(&self) -> &Path {
self.path.as_path()
}
}
fn write_tree(path: &Path, tree: serde_json::Value) {
use serde_json::Value;
use std::fs;
if let Value::Object(map) = tree {
for (name, contents) in map {
let mut path = PathBuf::from(path);
path.push(name);
match contents {
Value::Object(_) => {
fs::create_dir(&path).unwrap();
if path.file_name() == Some(OsStr::new(".git")) {
git2::Repository::init(path.parent().unwrap()).unwrap();
}
write_tree(&path, contents);
}
Value::Null => {
fs::create_dir(&path).unwrap();
}
Value::String(contents) => {
fs::write(&path, contents).unwrap();
}
_ => {
panic!("JSON object must contain only objects, strings, or null");
}
}
}
} else {
panic!("You must pass a JSON object to this helper")
}
}
pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
let mut text = String::new();
for row in 0..rows {
let c: char = (start_char as u32 + row as u32) as u8 as char;
let mut line = c.to_string().repeat(cols);
if row < rows - 1 {
line.push('\n');
}
text += &line;
}
text
}
-62
View File
@@ -1,62 +0,0 @@
pub enum SetEqError<T> {
LeftMissing(T),
RightMissing(T),
}
impl<T> SetEqError<T> {
pub fn map<R, F: FnOnce(T) -> R>(self, update: F) -> SetEqError<R> {
match self {
SetEqError::LeftMissing(missing) => SetEqError::LeftMissing(update(missing)),
SetEqError::RightMissing(missing) => SetEqError::RightMissing(update(missing)),
}
}
}
#[macro_export]
macro_rules! set_eq {
($left:expr,$right:expr) => {{
use util::test::*;
let left = $left;
let right = $right;
let mut result = Ok(());
for right_value in right.iter() {
if !left.contains(right_value) {
result = Err(SetEqError::LeftMissing(right_value.clone()));
break;
}
}
if result.is_ok() {
for left_value in left.iter() {
if !right.contains(left_value) {
result = Err(SetEqError::RightMissing(left_value.clone()));
}
}
}
result
}};
}
#[macro_export]
macro_rules! assert_set_eq {
($left:expr,$right:expr) => {{
use util::test::*;
use util::set_eq;
let left = $left;
let right = $right;
match set_eq!(&left, &right) {
Err(SetEqError::LeftMissing(missing)) => {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nleft does not contain {:?}", &left, &right, &missing);
},
Err(SetEqError::RightMissing(missing)) => {
panic!("assertion failed: `(left == right)`\n left: {:?}\nright: {:?}\nright does not contain {:?}", &left, &right, &missing);
},
_ => {}
}
}};
}

Some files were not shown because too many files have changed in this diff Show More