Compare commits

...

57 Commits
v1.2.0 ... main

Author SHA1 Message Date
Leonard Hecker
524b22c3c5
Fix warnings when building on macOS (#754) 2026-01-28 00:12:20 +01:00
Leonard Hecker
e5a92eb7d6
Fix the latest clippy warning (#751) 2026-01-26 23:30:31 +01:00
Leonard Hecker
ad8415fd44
Clean up error handling (#745)
* Use `io::Error` for OS error handling
  (This results in some added bloat, but is more convenient for now.)
* Add an `icu` error type
* Move `apperr` from the library into the binary

This also sneaks in a minor improvement to sorting.
The only two places we need to sort, don't need stable sorting.
(Unstable sorting is a lot faster and simpler.)
2026-01-23 13:57:42 +01:00
Leonard Hecker
9bc86c5acd
Add a glob matcher (#743)
Supports `*` and `**` patterns. That's enough for our upcoming purposes.
2026-01-22 19:49:04 +01:00
Leonard Hecker
6b6a4b2d9f
Add a JSON parser (#742)
This is a bog-standard JSONC parser. Not much to be said.
Its performance is quite alright.

Depends on #741
2026-01-21 00:05:09 +01:00
Leonard Hecker
8d9b1ede11
Add proper multithreading support to Arena (#741)
The main change is adding multithreading support in order to make JSON
unit tests work properly. The TLS overhead is not _that bad.

Other changes:
* Switch to `io::Result`, because `AllocError` doesn't
  transmit error codes (meh!)
* Reduce x86 commit chunk size to 32KiB
* Improved performance slightly by inlining harder (`alloc_uninit`)
  and outlining the result unwrap (`alloc_raw_bump`)
2026-01-20 15:11:44 -06:00
Sergio Triana Escobedo
d0db071474
Create parent directories when saving to a non-existent path (#738)
Closes #737

Co-authored-by: Leonard Hecker <leonard@hecker.io>
2026-01-20 13:10:55 -06:00
Cal
48c2ab2949
Use platform line ending when opening single line files (#739) 2026-01-13 15:33:37 +00:00
Leonard Hecker
dbd74656a9
Fix colors with Terminal.app's Clear Dark theme (#728) 2026-01-05 17:50:36 +01:00
Stefan
fa2b1b88cc
Update man page for edit command to version 1.2.1 (#727)
Alt does not work in macOS.
2026-01-05 14:12:26 +00:00
viyic
364579a045
Add Indonesian translation (#629) 2025-12-01 21:33:40 +00:00
Leonard Hecker
3df9e7cb6c
Fix multiple issues found under Linux (#706)
Fixed:
* `sighandler_t` warning in nightly
* cppdbg + gdb pretty printing
* UTF8 parsing for SGR mouse coords
2025-12-01 15:24:27 -06:00
VenusGirl❤
d71e94b303
Improve Korean translations (#663) 2025-12-01 22:17:14 +01:00
Mr Flamel
ca661a2901
i18n: Add Estonian translations (#693) 2025-12-01 22:16:34 +01:00
Mohammad Abu Mattar
1601c3f592
i18n: Add Arabic translations (#634)
Co-authored-by: MKAbuMattar <mohamamd.khaled@outlook.com>
2025-12-01 22:15:54 +01:00
Leonard Hecker
5f284a1df9
Move arena & helpers into their own crate (#694)
This will allow us to use the `Arena` in `build.rs`.
This changeset also contains a version bump of all dependencies.
2025-12-01 21:51:55 +01:00
Tiago Mouta
82cc84a610
Add Portuguese (pt-PT) translations (#688) 2025-10-31 21:09:55 +01:00
Miteigi
c1adb12b52
Add Vietnamese translations (#669) 2025-10-17 19:08:57 +02:00
Leonard Hecker
67c401648f
Fix Rust nightly builds (#668)
`panic_immediate_abort` is being stablized as `panic = immediate-abort`
(yay!).
See: https://github.com/rust-lang/rust/issues/147286

Closes #657
2025-10-14 19:53:51 +02:00
Emir SARI
c557cdbf04
Update Turkish translations (#655) 2025-10-13 14:47:17 +00:00
Dustin L. Howett
bb569841c9
sys/win: display a useful error message when SetConsoleMode fails (#639)
edit will now display specific error messages when the console fails to
support `ENABLE_VIRTUAL_TERMINAL_INPUT`. The user will be gently
reprimanded for not using the modern console host.

It is technically possible to run edit on OpenConsole (or another third-
party console host!)--even on Windows 8.1--where it will work properly.
2025-09-09 15:24:09 -05:00
Dustin L. Howett
51d19c9487
windows: fix the compatibility section of the manifest (#635)
So, it turns out that `supportedOS` was being ignored because it was
taken to be in the default `asm.v1` namespace. :)
2025-09-05 12:58:47 -05:00
hev
ccfebb274e
Replace vseq/vand with their immediate-form variants (#630) 2025-08-29 00:31:10 +02:00
four-poetic-drew
c8fec86709
Allow opening directories via the CLI (#577)
Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-08-28 00:49:45 +02:00
Leonard Hecker
a3a6f5f8be
Various minor improvements (#625) 2025-08-25 13:18:00 -05:00
Leonard Hecker
7338c3cbbc
Move build.rs into its own directory (#623) 2025-08-25 12:54:53 -05:00
Leonard Hecker
695d88e631
Fix alpha blending formula (#594)
Since `srgb_to_linear` is non-linear we can't use it for premultiplied
colors. Instead of unpremultiplying them, I changed the rest of the app
to straight alpha and introduced types to ensure we don't mess it up.
2025-08-12 23:27:02 +02:00
Alexandru Spînu
f17552c8f6
Fix #485: Reject invalid args and allow positional args (#503)
Closes #485

Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-08-12 23:25:15 +02:00
Leonard Hecker
1b7298c3b3
Separate scrolling from clicking (#603)
Closes #410
Closes #588
2025-08-12 23:22:38 +02:00
Leonard Hecker
a41267af47
Fix ctrl modifier detection for mouse input (#604) 2025-08-12 19:20:24 +02:00
hev
e2ea892426
Use unified data types in LoongArch SIMD intrinsics (#602) 2025-08-12 11:57:43 +00:00
Aloïs MASSON--CLAUDEZ
f6ca0e68ca
Fix CONTRIBUTING.md localization path (#601) 2025-08-05 21:09:40 +00:00
Aaron J Prisk
4a34873ec3
Add snapcraft.yaml (#500) 2025-08-04 21:26:26 +00:00
Leonard Hecker
4f4d093357
Fix undo grouping of backspacing (#590)
This regressed in 091b742.
2025-08-04 22:32:42 +02:00
Leonard Hecker
5962e31a83
Merge all localization PRs (#596)
I hope I got all the names right as I had to pull them out manually.
The majority of PRs required changes to make them work.

Closes #108
Closes #114
Closes #125
Closes #130
Closes #144
Closes #148
Closes #314
Closes #358
Closes #373
Closes #402
Closes #437
Closes #465
Closes #497
Closes #502
Closes #514
Closes #585

Co-authored-by: Ebrahim Byagowi <ebrahim@gnu.org>
Co-authored-by: Mads Ohm Larsen <mads.ohm@gmail.com>
Co-authored-by: william.beino <william.beino@gamifiera.com>
Co-authored-by: marinac-dev <github@regex-sh.com>
Co-authored-by: marginal23326 <58261815+marginal23326@users.noreply.github.com>
Co-authored-by: NandeMD <celikumutcan2001@gmail.com>
Co-authored-by: Mekan Soltanov <mknsltnw@gmail.com>
Co-authored-by: Þór Sigurðsson <thor@ttk.is>
Co-authored-by: Skryta Istota <48822204+hidden-being@users.noreply.github.com>
Co-authored-by: Piotr Błażejewicz <peterblazejewicz@users.noreply.github.com>
Co-authored-by: schilive <schilive100@gmail.com>
Co-authored-by: Jakub Kolčář <j.kolcar@raynet.cz>
Co-authored-by: Michael Avagianos <hello@maikkun.dev>
Co-authored-by: Ruzsinszki Gábor <ruzsinszki.gabor@gmail.com>
Co-authored-by: erithax <erithax@proton.me>
Co-authored-by: spinualexandru <spinualexandru@outlook.com>
Co-authored-by: Ronja Koistinen <ronja.koistinen@helsinki.fi>
Co-authored-by: ykarpenko <ykarpenko@netfully.com>
2025-08-01 01:05:27 +02:00
Leonard Hecker
3c41b85ae4
Compile localizations with build.rs (#591) 2025-07-31 23:36:49 +02:00
Leonard Hecker
63d2574774
Fix lines_bwd unit test (#574)
The implementation is correct, and the test was wrong.
TTD fans in shambles rn.

Closes #566
2025-07-10 00:31:52 +02:00
luisgizirian
2f48091708
Add initial devcontainer configuration for Rust environment (#496)
Closes #494

Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-07-09 22:32:17 +02:00
Nukleari
e16b4abffc
feat: improve desktop file (#560)
Closes #549
2025-07-09 20:30:53 +00:00
Leonard Hecker
a43e4723e6
Fix printing help/version without TTY (#556) 2025-07-09 15:27:11 -05:00
adamjoer
dd61854ad5
Indicate unsaved work with U+25CF in the terminal title (#523)
Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-07-02 18:23:36 +02:00
hev
e9ad75685f
Add SIMD impl of memset for LoongArch (#547) 2025-07-02 15:55:19 +00:00
hev
259a198dc0
Add SIMD impl of memchr2 for LoongArch (#551) 2025-07-02 17:55:15 +02:00
hev
75a7d76072
Add SIMD impls of lines_fwd and lines_bwd for LoongArch (#539) 2025-07-02 17:53:03 +02:00
recently-avoid-dyin
71b97e95f5
Add Ctrl+L shortcut to select whole line (#541) 2025-07-02 15:20:51 +00:00
hev
d27ba98684
Optimize SIMD impl of lines_fwd and lines_bwd (#535) 2025-07-02 14:43:01 +00:00
hev
37b18c382e
Enable default features for build-std (#554)
Closes #553
2025-07-02 12:51:16 +00:00
Leonard Hecker
c34bdb0e81
Fix file truncation when writing with ICU (#548) 2025-06-30 23:41:23 +02:00
Mani Tofigh
7ece8c5a38
Fix UTF-8 buffer prepending in read_stdin (#520) 2025-06-25 15:59:06 +02:00
Brian
b4cd1f6668
Support for multiline indentation (#245)
Closes #110

Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-06-24 02:05:48 +02:00
pheonick
796209ff48
Make whitespace inside selections visible (#397)
This PR additionally adds support for escaping C1 control codes.

Closes #34

Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-06-19 17:20:10 -05:00
Julio Ramirez
091b74240c
Move lines with Alt+Up/Down (#230)
Closes #27

Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-06-19 17:19:05 -05:00
Leonard Hecker
b277a1e67b
Make the ICU SONAME configurable (#495)
This PR was tested on Ubuntu with:
```
EDIT_CFG_ICUUC_SONAME=libicuuc.so.74
EDIT_CFG_ICUI18N_SONAME=libicui18n.so.74
EDIT_CFG_ICU_RENAMING_VERSION=74
cargo build --config .cargo/release.toml --release
```

Search & Replace now works flawlessly. I hope that package maintainers
will be able to make use of this when ingesting future versions of Edit.

Closes #172
2025-06-19 23:26:05 +02:00
viyic
70f5b73878
Add support for captured groups in Find & Replace (#222)
Closes #111

Co-authored-by: Leonard Hecker <leonard@hecker.io>
2025-06-17 18:51:29 -05:00
Leonard Hecker
91a9a5f808
Bump to v1.2.1 (#491)
The only change will be the inclusion of the app icon.
2025-06-17 09:59:15 -07:00
Dustin L. Howett
11ee7f0b64
assets: update to a smaller icon file (thanks Leonard!) (#478)
It is 16kb and contains the 256x256 image as well.
2025-06-14 00:02:04 +02:00
Dustin L. Howett
c44eb4297f
Add the final icon in SVG and ICO (with the full size ramp) format (#475)
Supersedes #139

Co-authored-by: Whitecat18 <smukx@5mukx.site>
2025-06-13 01:25:03 +02:00
85 changed files with 7018 additions and 3355 deletions

View File

@ -0,0 +1,16 @@
[profile.release]
panic = "immediate-abort"
[target.'cfg(all(target_os = "windows", target_env = "msvc"))']
rustflags = [
"-Ctarget-feature=+crt-static",
"-Clink-args=/DEFAULTLIB:ucrt.lib",
"-Clink-args=/NODEFAULTLIB:vcruntime.lib",
"-Clink-args=/NODEFAULTLIB:msvcrt.lib",
"-Clink-args=/NODEFAULTLIB:libucrt.lib",
]
[unstable]
panic-immediate-abort = true
build-std = ["std", "panic_abort"]
build-std-features = ["default", "optimize_for_size"]

View File

@ -20,4 +20,4 @@ rustflags = [
# = Huge reduction in binary size by removing all that.
[unstable]
build-std = ["std", "panic_abort"]
build-std-features = ["panic_immediate_abort", "optimize_for_size"]
build-std-features = ["default", "panic_immediate_abort", "optimize_for_size"]

View File

@ -0,0 +1,6 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/rust
{
"name": "Rust",
"image": "mcr.microsoft.com/devcontainers/rust:1-1-bookworm"
}

31
.vscode/launch.json vendored
View File

@ -2,7 +2,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch Debug (Windows)",
"name": "Launch edit (Windows)",
"preLaunchTask": "rust: cargo build",
"type": "cppvsdbg",
"request": "launch",
@ -10,30 +10,45 @@
"program": "${workspaceFolder}/target/debug/edit",
"cwd": "${workspaceFolder}",
"args": [
"${workspaceFolder}/src/bin/edit/main.rs"
"${workspaceFolder}/crates/edit/src/bin/edit/main.rs"
],
},
{
"name": "Launch Debug (GDB/LLDB)",
"name": "Launch edit (GDB, Linux)",
"preLaunchTask": "rust: cargo build",
"type": "cppdbg",
"request": "launch",
"miDebuggerPath": "rust-gdb",
"externalConsole": true,
"program": "${workspaceFolder}/target/debug/edit",
"cwd": "${workspaceFolder}",
"args": [
"${workspaceFolder}/src/bin/edit/main.rs"
"${workspaceFolder}/crates/edit/src/bin/edit/main.rs"
],
},
{
"name": "Launch Debug (LLDB)",
// NOTE for macOS: In order for this task to work you have to:
// 1. Run the "Fix externalConsole on macOS" task once
// 2. Add the following to your VS Code settings:
// "lldb-dap.environment": {
// "LLDB_LAUNCH_FLAG_LAUNCH_IN_TTY": "YES"
// }
"name": "Launch edit (lldb-dap, macOS)",
"preLaunchTask": "rust: cargo build",
"type": "lldb",
"type": "lldb-dap",
"request": "launch",
"program": "${workspaceFolder}/target/debug/edit",
"cwd": "${workspaceFolder}",
"args": [
"${workspaceFolder}/src/bin/edit/main.rs"
"${workspaceFolder}/crates/edit/src/bin/edit/main.rs"
],
}
},
{
// This is a workaround for https://github.com/microsoft/vscode-cpptools/issues/5079
"name": "Fix externalConsole on macOS",
"type": "node-terminal",
"request": "launch",
"command": "osascript -e 'tell application \"Terminal\"\ndo script \"echo hello\"\nend tell'"
},
]
}

View File

@ -2,7 +2,7 @@
## Translation improvements
You can find our translations in [`src/bin/edit/localization.rs`](./src/bin/edit/localization.rs).
You can find our translations in [`i18n/edit.toml`](./i18n/edit.toml).
Please feel free to open a pull request with your changes at any time.
If you'd like to discuss your changes first, please feel free to open an issue.

606
Cargo.lock generated
View File

@ -4,13 +4,31 @@ version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "alloca"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5a7d05ea6aea7e9e64d25b9156ba2fee3fdd659e34e41063cd2fc7cd020d7f4"
dependencies = [
"cc",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anes"
version = "0.1.6"
@ -19,27 +37,27 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.10"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "autocfg"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bumpalo"
version = "3.17.0"
version = "3.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
[[package]]
name = "cast"
@ -49,10 +67,11 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.25"
version = "1.2.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@ -60,9 +79,22 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "ciborium"
@ -93,18 +125,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.39"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.39"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstyle",
"clap_lex",
@ -112,24 +144,32 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.4"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "criterion"
version = "0.6.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
checksum = "4d883447757bb0ee46f233e9dc22eb84d93a9508c9b868687b274fc431d886bf"
dependencies = [
"alloca",
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"itertools 0.13.0",
"itertools",
"num-traits",
"oorandom",
"page_size",
"plotters",
"rayon",
"regex",
@ -141,12 +181,12 @@ dependencies = [
[[package]]
name = "criterion-plot"
version = "0.5.0"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
checksum = "ed943f81ea2faa8dcecbbfa50164acf95d555afec96a27871663b300e387b2e4"
dependencies = [
"cast",
"itertools 0.10.5",
"itertools",
]
[[package]]
@ -176,18 +216,18 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.3"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "edit"
version = "1.2.0"
version = "1.2.1"
dependencies = [
"criterion",
"libc",
"serde",
"serde_json",
"stdext",
"toml-span",
"windows-sys",
"winresource",
"zstd",
@ -200,56 +240,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "equivalent"
version = "1.0.2"
name = "find-msvc-tools"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "getrandom"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
"wasip2",
]
[[package]]
name = "half"
version = "2.6.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.15.3"
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"equivalent",
"hashbrown",
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "itertools"
version = "0.10.5"
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"either",
"cc",
]
[[package]]
name = "indoc"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
dependencies = [
"rustversion",
]
[[package]]
@ -263,15 +312,15 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.15"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jobserver"
version = "0.1.33"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
@ -279,9 +328,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.77"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@ -289,21 +338,21 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "log"
version = "0.4.27"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "memchr"
version = "2.7.4"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "num-traits"
@ -326,6 +375,22 @@ version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "page_size"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "pkg-config"
version = "0.3.32"
@ -362,33 +427,33 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.95"
version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rayon"
version = "1.10.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
@ -396,9 +461,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.12.1"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@ -406,9 +471,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.11.1"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
@ -418,9 +483,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.9"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
@ -429,21 +494,24 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "roxmltree"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb"
dependencies = [
"memchr",
]
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
@ -456,18 +524,28 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@ -476,23 +554,15 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.140"
version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
dependencies = [
"serde",
"serde_core",
"zmij",
]
[[package]]
@ -502,10 +572,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.101"
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "stdext"
version = "0.0.0"
dependencies = [
"libc",
]
[[package]]
name = "syn"
version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@ -523,51 +606,31 @@ dependencies = [
]
[[package]]
name = "toml"
version = "0.8.22"
name = "toml-span"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
checksum = "5c6532e5b62b652073bff0e2050ef57e4697a853be118d6c57c32b59fffdeaab"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
"smallvec",
]
[[package]]
name = "toml_datetime"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
name = "unicode-gen"
version = "0.0.0"
dependencies = [
"serde",
"anyhow",
"chrono",
"indoc",
"pico-args",
"rayon",
"roxmltree",
]
[[package]]
name = "toml_edit"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
[[package]]
name = "unicode-ident"
version = "1.0.18"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "version_check"
@ -586,45 +649,32 @@ dependencies = [
]
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
name = "wasip2"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen-rt",
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -632,146 +682,176 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.77"
version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
"windows-link",
]
[[package]]
name = "winresource"
version = "0.1.22"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a179ac8923651ff1d15efbee760b4dd3679fd85fa5a8b2bb1109b7248f80e30f"
checksum = "17cdfa8da4b111045a5e47c7c839e6c5e11c942de1309bc624393ed5d87f89c6"
dependencies = [
"toml",
"version_check",
]
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "zerocopy"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [
"bitflags",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
[[package]]
name = "zstd"
version = "0.13.3"
@ -792,9 +872,9 @@ dependencies = [
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
version = "2.0.16+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
dependencies = [
"cc",
"pkg-config",

View File

@ -1,20 +1,13 @@
[package]
name = "edit"
version = "1.2.0"
[workspace]
default-members = ["crates/edit"]
members = ["crates/*"]
resolver = "2"
[workspace.package]
edition = "2024"
rust-version = "1.87"
readme = "README.md"
repository = "https://github.com/microsoft/edit"
homepage = "https://github.com/microsoft/edit"
license = "MIT"
categories = ["text-editors"]
[[bench]]
name = "lib"
harness = false
[features]
debug-latency = []
repository = "https://github.com/microsoft/edit"
rust-version = "1.88"
# We use `opt-level = "s"` as it significantly reduces binary size.
# We could then use the `#[optimize(speed)]` attribute for spot optimizations.
@ -33,30 +26,7 @@ incremental = true # Improves re-compile times
codegen-units = 16 # Make compiling criterion faster (16 is the default, but profile.release sets it to 1)
lto = "thin" # Similarly, speed up linking by a ton
[dependencies]
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[target.'cfg(windows)'.build-dependencies]
winresource = "0.1.22"
[target.'cfg(windows)'.dependencies.windows-sys]
version = "0.59"
features = [
"Win32_Globalization",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_Console",
"Win32_System_Diagnostics_Debug",
"Win32_System_IO",
"Win32_System_LibraryLoader",
"Win32_System_Memory",
"Win32_System_Threading",
]
[dev-dependencies]
criterion = { version = "0.6", features = ["html_reports"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
zstd = { version = "0.13", default-features = false }
[workspace.dependencies]
edit = { path = "./crates/edit" }
stdext = { path = "./crates/stdext" }
unicode-gen = { path = "./crates/unicode-gen" }

View File

@ -19,18 +19,63 @@ You can install the latest version with WinGet:
winget install Microsoft.Edit
```
### Notes to Package Maintainers
The canonical executable name is "edit" and the alternative name is "msedit".
We're aware of the potential conflict of "edit" with existing commands and as such recommend naming packages and executables "msedit".
Names such as "ms-edit" should be avoided.
Assigning an "edit" alias is recommended if possible.
## Build Instructions
* [Install Rust](https://www.rust-lang.org/tools/install)
* Install the nightly toolchain: `rustup install nightly`
* Alternatively, set the environment variable `RUSTC_BOOTSTRAP=1`
* Clone the repository
* For a release build, run: `cargo build --config .cargo/release.toml --release`
* For a release build, run:
* Rust 1.90 or earlier: `cargo build --config .cargo/release.toml --release`
* otherwise: `cargo build --config .cargo/release-nightly.toml --release`
### Build Configuration
During compilation you can set various environment variables to configure the build. The following table lists the available configuration options:
Environment variable | Description
--- | ---
`EDIT_CFG_ICU*` | See [ICU library name (SONAME)](#icu-library-name-soname) for details.
`EDIT_CFG_LANGUAGES` | A comma-separated list of languages to include in the build. See [i18n/edit.toml](i18n/edit.toml) for available languages.
## Notes to Package Maintainers
### Package Naming
The canonical executable name is "edit" and the alternative name is "msedit".
We're aware of the potential conflict of "edit" with existing commands and recommend alternatively naming packages and executables "msedit".
Names such as "ms-edit" should be avoided.
Assigning an "edit" alias is recommended, if possible.
### ICU library name (SONAME)
This project _optionally_ depends on the ICU library for its Search and Replace functionality.
By default, the project will look for a SONAME without version suffix:
* Windows: `icuuc.dll`
* macOS: `libicuuc.dylib`
* UNIX, and other OS: `libicuuc.so`
If your installation uses a different SONAME, please set the following environment variable at build time:
* `EDIT_CFG_ICUUC_SONAME`:
For instance, `libicuuc.so.76`.
* `EDIT_CFG_ICUI18N_SONAME`:
For instance, `libicui18n.so.76`.
Additionally, this project assumes that the ICU exports are exported without `_` prefix and without version suffix, such as `u_errorName`.
If your installation uses versioned exports, please set:
* `EDIT_CFG_ICU_CPP_EXPORTS`:
If set to `true`, it'll look for C++ symbols such as `_u_errorName`.
Enabled by default on macOS.
* `EDIT_CFG_ICU_RENAMING_VERSION`:
If set to a version number, such as `76`, it'll look for symbols such as `u_errorName_76`.
Finally, you can set the following environment variables:
* `EDIT_CFG_ICU_RENAMING_AUTO_DETECT`:
If set to `true`, the executable will try to detect the `EDIT_CFG_ICU_RENAMING_VERSION` value at runtime.
The way it does this is not officially supported by ICU and as such is not recommended to be relied upon.
Enabled by default on UNIX (excluding macOS) if no other options are set.
To test your settings, run `cargo test` again but with the `--ignored` flag. For instance:
```sh
cargo test -- --ignored
```

View File

@ -4,7 +4,8 @@ Name=Microsoft Edit
GenericName=Text Editor
Comment=A simple editor for simple needs
Icon=edit
Exec=edit %U
Exec=edit %F
Terminal=true
MimeType=text/plain
Categories=Utility;TextEditor;
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
Keywords=text;editor

BIN
assets/edit.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,75 +1,148 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2349_313)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0918 19.0947L22.0855 15.0979C23.2589 14.5112 24.0001 13.3119 24.0001 12C24.0001 10.6881 23.2589 9.48882 22.0855 8.90213C22.6071 9.16293 22.4986 9.86016 21.977 10.121L15.5293 13.3448L14.0918 19.0947Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0918 19.0947L22.0855 15.0979C23.2589 14.5112 24.0001 13.3119 24.0001 12C24.0001 10.6881 23.2589 9.48882 22.0855 8.90213C22.6071 9.16293 22.4986 9.86016 21.977 10.121L15.5293 13.3448L14.0918 19.0947Z" fill="url(#paint0_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0918 19.0947L22.0855 15.0979C23.2589 14.5112 24.0001 13.3119 24.0001 12C24.0001 10.6881 23.2589 9.48882 22.0855 8.90213C22.6071 9.16293 22.4986 9.86016 21.977 10.121L15.5293 13.3448L14.0918 19.0947Z" fill="url(#paint1_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0918 19.0947L22.0855 15.0979C23.2589 14.5112 24.0001 13.3119 24.0001 12C24.0001 10.6881 23.2589 9.48882 22.0855 8.90213C22.6071 9.16293 22.4986 9.86016 21.977 10.121L15.5293 13.3448L14.0918 19.0947Z" fill="url(#paint2_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.47085 10.6552L9.90833 4.90526L1.91459 8.90213C0.741205 9.48882 0 10.6881 0 12C0 13.3119 0.741183 14.5112 1.91457 15.0979C1.39297 14.8371 1.50149 14.1398 2.02309 13.879L8.47085 10.6552Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.47085 10.6552L9.90833 4.90526L1.91459 8.90213C0.741205 9.48882 0 10.6881 0 12C0 13.3119 0.741183 14.5112 1.91457 15.0979C1.39297 14.8371 1.50149 14.1398 2.02309 13.879L8.47085 10.6552Z" fill="url(#paint3_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.47085 10.6552L9.90833 4.90526L1.91459 8.90213C0.741205 9.48882 0 10.6881 0 12C0 13.3119 0.741183 14.5112 1.91457 15.0979C1.39297 14.8371 1.50149 14.1398 2.02309 13.879L8.47085 10.6552Z" fill="url(#paint4_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75517 17.5181L7.87321 13.046L5.78126 12L2.02316 13.879C1.50156 14.1398 1.39302 14.8371 1.91462 15.0979L6.07921 17.1802L6.75517 17.5181Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75517 17.5181L7.87321 13.046L5.78126 12L2.02316 13.879C1.50156 14.1398 1.39302 14.8371 1.91462 15.0979L6.07921 17.1802L6.75517 17.5181Z" fill="url(#paint5_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.75517 17.5181L7.87321 13.046L5.78126 12L2.02316 13.879C1.50156 14.1398 1.39302 14.8371 1.91462 15.0979L6.07921 17.1802L6.75517 17.5181Z" fill="url(#paint6_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.127 10.9541L18.2189 12L21.977 10.121C22.4986 9.86017 22.6071 9.16294 22.0855 8.90214L17.9209 6.81985L17.245 6.48189L16.127 10.9541Z" fill="#D9D9D9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.127 10.9541L18.2189 12L21.977 10.121C22.4986 9.86017 22.6071 9.16294 22.0855 8.90214L17.9209 6.81985L17.245 6.48189L16.127 10.9541Z" fill="url(#paint7_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.127 10.9541L18.2189 12L21.977 10.121C22.4986 9.86017 22.6071 9.16294 22.0855 8.90214L17.9209 6.81985L17.245 6.48189L16.127 10.9541Z" fill="url(#paint8_linear_2349_313)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.127 10.9541L18.2189 12L21.977 10.121C22.4986 9.86017 22.6071 9.16294 22.0855 8.90214L17.9209 6.81985L17.245 6.48189L16.127 10.9541Z" fill="url(#paint9_linear_2349_313)"/>
<path d="M11.9878 19.9576L10.0194 23.5133C9.85317 23.8136 9.53698 24 9.19374 24C8.67253 24 8.25 23.5775 8.25 23.0563V18.6577C8.25 18.2209 8.30357 17.7857 8.40951 17.362L12.3366 1.65341C12.5796 0.68169 13.4527 0 14.4543 0C15.8744 0 16.9164 1.33455 16.5719 2.71223L12.7344 18.0622C12.569 18.7238 12.318 19.361 11.9878 19.9576Z" fill="url(#paint10_linear_2349_313)"/>
<path d="M11.9878 19.9576L10.0194 23.5133C9.85317 23.8136 9.53698 24 9.19374 24C8.67253 24 8.25 23.5775 8.25 23.0563V18.6577C8.25 18.2209 8.30357 17.7857 8.40951 17.362L12.3366 1.65341C12.5796 0.68169 13.4527 0 14.4543 0C15.8744 0 16.9164 1.33455 16.5719 2.71223L12.7344 18.0622C12.569 18.7238 12.318 19.361 11.9878 19.9576Z" fill="url(#paint11_linear_2349_313)"/>
</g>
<defs>
<linearGradient id="paint0_linear_2349_313" x1="22.2355" y1="13.1286" x2="15.8564" y2="16.1088" gradientUnits="userSpaceOnUse">
<stop stop-color="#3DCBFF"/>
<stop offset="1" stop-color="#0091EB"/>
</linearGradient>
<linearGradient id="paint1_linear_2349_313" x1="22.2355" y1="13.1286" x2="15.8564" y2="16.1088" gradientUnits="userSpaceOnUse">
<stop stop-color="#3BD5FF"/>
<stop offset="1" stop-color="#3DCBFF"/>
</linearGradient>
<linearGradient id="paint2_linear_2349_313" x1="24.0001" y1="12.1487" x2="15.1349" y2="17.5577" gradientUnits="userSpaceOnUse">
<stop stop-color="#76EB95"/>
<stop offset="1" stop-color="#309C61"/>
</linearGradient>
<linearGradient id="paint3_linear_2349_313" x1="8.14375" y1="9.13175" x2="1.76459" y2="12.1119" gradientUnits="userSpaceOnUse">
<stop stop-color="#3BD5FF"/>
<stop offset="1" stop-color="#3DCBFF"/>
</linearGradient>
<linearGradient id="paint4_linear_2349_313" x1="9.90833" y1="8.15188" x2="1.04312" y2="13.5609" gradientUnits="userSpaceOnUse">
<stop stop-color="#309C61"/>
<stop offset="1" stop-color="#76EB95"/>
</linearGradient>
<linearGradient id="paint5_linear_2349_313" x1="7.1966" y1="16.0181" x2="3.19388" y2="16.6496" gradientUnits="userSpaceOnUse">
<stop stop-color="#3DCBFF"/>
<stop offset="1" stop-color="#0FAFFF"/>
</linearGradient>
<linearGradient id="paint6_linear_2349_313" x1="7.87321" y1="16.2896" x2="5.53862" y2="11.3533" gradientUnits="userSpaceOnUse">
<stop stop-color="#52D17C"/>
<stop offset="1" stop-color="#1E794A"/>
</linearGradient>
<linearGradient id="paint7_linear_2349_313" x1="21.75" y1="10.5" x2="17.7473" y2="11.1315" gradientUnits="userSpaceOnUse">
<stop stop-color="#0078D4"/>
<stop offset="1" stop-color="#0FAFFF"/>
</linearGradient>
<linearGradient id="paint8_linear_2349_313" x1="21.75" y1="10.5" x2="17.7473" y2="11.1315" gradientUnits="userSpaceOnUse">
<stop stop-color="#0FAFFF"/>
<stop offset="1" stop-color="#3DCBFF"/>
</linearGradient>
<linearGradient id="paint9_linear_2349_313" x1="22.4266" y1="10.7714" x2="20.0921" y2="5.83518" gradientUnits="userSpaceOnUse">
<stop stop-color="#1E794A"/>
<stop offset="1" stop-color="#52D17C"/>
</linearGradient>
<linearGradient id="paint10_linear_2349_313" x1="11.25" y1="10.5" x2="15.5195" y2="11.6079" gradientUnits="userSpaceOnUse">
<stop stop-color="#0FAFFF"/>
<stop offset="0.245" stop-color="#3BD5FF"/>
<stop offset="1" stop-color="#0078D4"/>
</linearGradient>
<linearGradient id="paint11_linear_2349_313" x1="14.1714" y1="12.5" x2="10.4649" y2="11.6493" gradientUnits="userSpaceOnUse">
<stop offset="0.137772" stop-color="#52D17C"/>
<stop offset="0.75" stop-color="#B6F6C7"/>
<stop offset="1" stop-color="#76EB95"/>
</linearGradient>
<clipPath id="clip0_2349_313">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
version="1.1"
id="svg17"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<g
clip-path="url(#clip0_4303_1230)"
id="g6"
transform="scale(0.25)">
<g
clip-path="url(#clip1_4303_1230)"
id="g5">
<path
d="M 0.999512,42.4717 C 0.999876,39.4985 3.12964,37.564 5.78905,38.8936 L 31.998,51.999 27.5547,69.7783 2.21092,57.1064 C 0.855902,56.4289 0.99966,55.0432 0.999512,53.5283 Z"
fill="url(#paint0_linear_4303_1230)"
id="path1"
style="fill:url(#paint0_linear_4303_1230)" />
<path
d="M 93.7891,34.8945 C 95.1441,35.5721 95,36.9576 95,38.4727 v 11.0546 c 0,2.9736 -2.1295,4.908 -4.7891,3.5782 L 64,40 68.4434,22.2207 Z"
fill="url(#paint1_linear_4303_1230)"
id="path2"
style="fill:url(#paint1_linear_4303_1230)" />
<path
d="M 93.7891,34.8945 C 95.1441,35.5721 96,36.9576 96,38.4727 v 12.5839 c -3e-4,3.0297 -1.7121,5.7993 -4.4219,7.1543 L 54.8574,76.5703 60.5703,53.7129 93.1055,37.4473 C 93.6536,37.1732 94,36.6129 94,36 94,35.3871 93.6536,34.8268 93.1055,34.5527 Z"
fill="url(#paint2_linear_4303_1230)"
id="path3"
style="fill:url(#paint2_linear_4303_1230)" />
<path
d="M 35.4277,38.2832 2.89453,54.5527 C 2.34637,54.8268 2,55.3871 2,56 c 1e-5,0.6129 0.34637,1.1732 0.89453,1.4473 L 2.21094,57.1055 C 0.855918,56.4279 0,55.0424 0,53.5273 V 40.9434 C 3.44787e-4,37.9137 1.71212,35.1441 4.42188,33.7891 L 41.1426,15.4277 Z"
fill="url(#paint3_linear_4303_1230)"
id="path4"
style="fill:url(#paint3_linear_4303_1230)" />
<path
d="M 48.5263,77.8978 65.4392,10.2462 C 66.7403,5.04164 62.8039,0 57.4392,0 c -3.784,0 -7.0823,2.57527 -8,6.24621 L 32.3719,74.5152 c -0.2456,0.9825 -0.3197,2 -0.2189,3.0078 l 1.6589,16.5892 c 0.1072,1.0717 1.009,1.8878 2.086,1.8878 0.6904,0 1.3365,-0.3399 1.7276,-0.9087 l 9.1476,-13.3056 c 0.8118,-1.1809 1.4056,-2.4977 1.7532,-3.8879 z"
fill="url(#paint4_linear_4303_1230)"
id="path5"
style="fill:url(#paint4_linear_4303_1230)" />
</g>
</g>
<defs
id="defs17">
<linearGradient
id="paint0_linear_4303_1230"
x1="-0.00048816099"
y1="46.764801"
x2="38.393902"
y2="65.956398"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#136C6C"
id="stop6" />
<stop
offset="1"
stop-color="#1A7F7C"
id="stop7" />
</linearGradient>
<linearGradient
id="paint1_linear_4303_1230"
x1="57.999298"
y1="26.713499"
x2="96.372597"
y2="45.436699"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#1A7F7C"
id="stop8" />
<stop
offset="1"
stop-color="#136C6C"
id="stop9" />
</linearGradient>
<linearGradient
id="paint2_linear_4303_1230"
x1="96"
y1="44.551102"
x2="49.908199"
y2="67.877701"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#2BDABE"
id="stop10" />
<stop
offset="1"
stop-color="#1EC8B0"
id="stop11" />
</linearGradient>
<linearGradient
id="paint3_linear_4303_1230"
x1="45.636398"
y1="23.2024"
x2="-0.481435"
y2="46.417"
gradientUnits="userSpaceOnUse">
<stop
stop-color="#1EC8B0"
id="stop12" />
<stop
offset="1"
stop-color="#2BDABE"
id="stop13" />
</linearGradient>
<linearGradient
id="paint4_linear_4303_1230"
x1="55.686501"
y1="50"
x2="37.383999"
y2="45.5784"
gradientUnits="userSpaceOnUse">
<stop
offset="0.137772"
stop-color="#0067BF"
id="stop14" />
<stop
offset="0.682692"
stop-color="#0FAFFF"
id="stop15" />
<stop
offset="0.836727"
stop-color="#0094F0"
id="stop16" />
</linearGradient>
<clipPath
id="clip0_4303_1230">
<rect
width="96"
height="96"
fill="#ffffff"
id="rect16"
x="0"
y="0" />
</clipPath>
<clipPath
id="clip1_4303_1230">
<rect
width="96"
height="96"
fill="#ffffff"
id="rect17"
x="0"
y="0" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,26 @@
{
// Object with various value types
"string": "Hello, world!", // string literal
"numberInt": 42, // integer number
"numberFloat": -3.14e+2, // floating point with exponent
"booleanTrue": true, // boolean true
"booleanFalse": false, // boolean false
"nullValue": null, // null literal
"array": [
"item1", // string in array
2, // number in array
false, // boolean in array
null, // null in array
{
"nested": "object"
} // object in array
],
"emptyObject": {}, // empty object
"emptyArray": [], // empty array
/* Multi-line comment:
This is a block comment
inside JSONC.
*/
"unicodeString": "Emoji: \uD83D\uDE03", // Unicode escape
"escapedChars": "Line1\nLine2\tTabbed\\Backslash\"Quote" // Escaped characters
}

View File

@ -1,4 +1,4 @@
.TH EDIT 1 "version 1.0" "May 2025"
.TH EDIT 1 "version 1.2.1" "December 2025"
.SH NAME
edit \- a simple text editor
.SH SYNOPSIS
@ -6,7 +6,7 @@ edit \- a simple text editor
.SH DESCRIPTION
edit is a simple text editor inspired by MS-DOS edit.
.SH EDITING
Edit is an interactive mode-less editor. Use Alt-F to access the menus.
Edit is an interactive mode-less editor. Use F10 to access the menus.
.SH ARGUMENTS
.TP
\fIFILE[:LINE[:COLUMN]]\fP
@ -19,8 +19,9 @@ Print the help message.
\fB\-v\fP, \fB\-\-version\fP
Print the version number.
.SH COPYRIGHT
Copyright (c) Microsoft Corporation.
Copyright \(co Microsoft Corporation.
.br
Licensed under the MIT License.
.SH SEE ALSO
https://github.com/microsoft/edit
.UR https://github.com/microsoft/edit
.UE

25
assets/snapcraft.yaml Normal file
View File

@ -0,0 +1,25 @@
name: msedit
base: core24
version: '1.2.1'
summary: Edit is an MS-DOS inspired text editor from Microsoft
description: |
Edit pays homage to the classic MS-DOS Editor, but with a modern interface and input controls similar to VS Code. Learn more at https://github.com/microsoft/edit
Disclaimer: This is an unofficial Snap and it is not endorsed by nor affiliated officially with Microsoft Corporation.
grade: stable
confinement: strict
apps:
msedit:
command: bin/edit
plugs:
- home
parts:
edit:
source: https://github.com/microsoft/edit.git
source-type: git
plugin: rust
build-packages:
- build-essential

View File

@ -1,14 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
fn main() {
#[cfg(windows)]
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
winresource::WindowsResource::new()
.set_manifest_file("src/bin/edit/edit.exe.manifest")
.set("FileDescription", "Microsoft Edit")
.set("LegalCopyright", "© Microsoft Corporation. All rights reserved.")
.compile()
.unwrap();
}
}

50
crates/edit/Cargo.toml Normal file
View File

@ -0,0 +1,50 @@
[package]
name = "edit"
version = "1.2.1"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
build = "build/main.rs"
categories = ["text-editors"]
[[bench]]
name = "lib"
harness = false
[features]
# Display editor latency in the top-right corner
debug-latency = []
[dependencies]
stdext.workspace = true
[target.'cfg(unix)'.dependencies]
libc = "0.2"
[build-dependencies]
stdext.workspace = true
# The default toml crate bundles its dependencies with bad compile times. Thanks.
# Thankfully toml-span exists. FWIW the alternative is yaml-rust (without the 2 suffix).
toml-span = { version = "0.6", default-features = false }
[target.'cfg(windows)'.build-dependencies]
winresource = { version = "0.1", default-features = false }
[target.'cfg(windows)'.dependencies.windows-sys]
version = "0.61"
features = [
"Win32_Globalization",
"Win32_Security",
"Win32_Storage_FileSystem",
"Win32_System_Console",
"Win32_System_IO",
"Win32_System_LibraryLoader",
"Win32_System_Threading",
]
[dev-dependencies]
criterion = { version = "0.8", features = ["html_reports"] }
zstd = { version = "0.13", default-features = false }

View File

@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![feature(allocator_api)]
use std::hint::black_box;
use std::io::Cursor;
use std::{mem, vec};
@ -8,32 +10,59 @@ use std::{mem, vec};
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use edit::helpers::*;
use edit::simd::MemsetSafe;
use edit::{arena, buffer, hash, oklab, simd, unicode};
use serde::Deserialize;
use edit::{buffer, glob, hash, json, oklab, simd, unicode};
use stdext::arena::{self, Arena, scratch_arena};
#[derive(Deserialize)]
pub struct EditingTracePatch(pub usize, pub usize, pub String);
struct EditingTracePatch<'a>(usize, usize, &'a str);
#[derive(Deserialize)]
pub struct EditingTraceTransaction {
pub patches: Vec<EditingTracePatch>,
struct EditingTraceTransaction<'a> {
patches: Vec<EditingTracePatch<'a>, &'a Arena>,
}
#[derive(Deserialize)]
pub struct EditingTraceData {
#[serde(rename = "startContent")]
pub start_content: String,
#[serde(rename = "endContent")]
pub end_content: String,
pub txns: Vec<EditingTraceTransaction>,
struct EditingTraceData<'a> {
start_content: &'a str,
end_content: &'a str,
txns: Vec<EditingTraceTransaction<'a>, &'a Arena>,
}
fn bench_buffer(c: &mut Criterion) {
let data = include_bytes!("../assets/editing-traces/rustcode.json.zst");
let data = zstd::decode_all(Cursor::new(data)).unwrap();
let data: EditingTraceData = serde_json::from_slice(&data).unwrap();
let mut patches_with_coords = Vec::new();
let scratch = scratch_arena(None);
let data = {
let data = include_bytes!("../../../assets/editing-traces/rustcode.json.zst");
let data = zstd::decode_all(Cursor::new(data)).unwrap();
let data = str::from_utf8(&data).unwrap();
let data = json::parse(&scratch, data).unwrap();
let root = data.as_object().unwrap();
let txns = root.get_array("txns").unwrap();
let mut res = EditingTraceData {
start_content: root.get_str("startContent").unwrap(),
end_content: root.get_str("endContent").unwrap(),
txns: Vec::with_capacity_in(txns.len(), &scratch),
};
for txn in txns {
let txn = txn.as_object().unwrap();
let patches = txn.get_array("patches").unwrap();
let mut txn =
EditingTraceTransaction { patches: Vec::with_capacity_in(patches.len(), &scratch) };
for patch in patches {
let patch = patch.as_array().unwrap();
let offset = patch[0].as_number().unwrap() as usize;
let del_len = patch[1].as_number().unwrap() as usize;
let ins_str = patch[2].as_str().unwrap();
txn.patches.push(EditingTracePatch(offset, del_len, ins_str));
}
res.txns.push(txn);
}
res
};
let mut patches_with_coords = Vec::new();
{
let mut tb = buffer::TextBuffer::new(false).unwrap();
tb.set_crlf(false);
@ -47,7 +76,7 @@ fn bench_buffer(c: &mut Criterion) {
tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType);
tb.write_raw(p.2.as_bytes());
patches_with_coords.push((beg, p.1 as CoordType, p.2.clone()));
patches_with_coords.push((beg, p.1 as CoordType, p.2));
}
}
@ -106,6 +135,15 @@ fn bench_buffer(c: &mut Criterion) {
});
}
fn bench_glob(c: &mut Criterion) {
// Same benchmark as in glob-match
const PATH: &str = "foo/bar/foo/bar/foo/bar/foo/bar/foo/bar.txt";
const GLOB: &str = "foo/**/bar.txt";
c.benchmark_group("glob")
.bench_function("glob_match", |b| b.iter(|| assert!(glob::glob_match(GLOB, PATH))));
}
fn bench_hash(c: &mut Criterion) {
c.benchmark_group("hash")
.throughput(Throughput::Bytes(8))
@ -125,11 +163,31 @@ fn bench_hash(c: &mut Criterion) {
});
}
fn bench_json(c: &mut Criterion) {
let str = include_str!("../../../assets/highlighting-tests/json.json");
c.benchmark_group("json").throughput(Throughput::Bytes(str.len() as u64)).bench_function(
"parse",
|b| {
b.iter(|| {
let scratch = scratch_arena(None);
let obj = json::parse(&scratch, black_box(str)).unwrap();
black_box(obj);
})
},
);
}
fn bench_oklab(c: &mut Criterion) {
c.benchmark_group("oklab")
.bench_function("srgb_to_oklab", |b| b.iter(|| oklab::srgb_to_oklab(black_box(0xff212cbe))))
.bench_function("oklab_blend", |b| {
b.iter(|| oklab::oklab_blend(black_box(0x7f212cbe), black_box(0x7f3aae3f)))
.bench_function("StraightRgba::as_oklab", |b| {
b.iter(|| black_box(oklab::StraightRgba::from_le(0xff212cbe)).as_oklab())
})
.bench_function("StraightRgba::oklab_blend", |b| {
b.iter(|| {
black_box(oklab::StraightRgba::from_le(0x7f212cbe))
.oklab_blend(black_box(oklab::StraightRgba::from_le(0x7f3aae3f)))
})
});
}
@ -225,7 +283,9 @@ fn bench(c: &mut Criterion) {
arena::init(128 * MEBI).unwrap();
bench_buffer(c);
bench_glob(c);
bench_hash(c);
bench_json(c);
bench_oklab(c);
bench_simd_lines_fwd(c);
bench_simd_memchr2(c);

View File

@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::env::VarError;
pub fn env_opt(name: &str) -> String {
match std::env::var(name) {
Ok(value) => value,
Err(VarError::NotPresent) => String::new(),
Err(VarError::NotUnicode(_)) => {
panic!("Environment variable `{name}` is not valid Unicode")
}
}
}

204
crates/edit/build/i18n.rs Normal file
View File

@ -0,0 +1,204 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Write as _;
use crate::helpers::env_opt;
pub fn generate(definitions: &str) -> String {
let i18n = toml_span::parse(definitions).expect("Failed to parse i18n file");
let root = i18n.as_table().unwrap();
let mut languages = Vec::new();
let mut aliases = Vec::new();
let mut translations: BTreeMap<String, HashMap<String, String>> = BTreeMap::new();
for (k, v) in root.iter() {
match &k.name[..] {
"__default__" => {
const ERROR: &str = "i18n: __default__ must be [str]";
languages = Vec::from_iter(
v.as_array()
.expect(ERROR)
.iter()
.map(|lang| lang.as_str().expect(ERROR).to_string()),
);
}
"__alias__" => {
const ERROR: &str = "i18n: __alias__ must be str->str";
aliases.extend(v.as_table().expect(ERROR).iter().map(|(alias, lang)| {
(alias.to_string(), lang.as_str().expect(ERROR).to_string())
}));
}
_ => {
const ERROR: &str = "i18n: LocId must be str->str";
translations.insert(
k.name.to_string(),
HashMap::from_iter(
v.as_table().expect(ERROR).iter().map(|(k, v)| {
(k.name.to_string(), v.as_str().expect(ERROR).to_string())
}),
),
);
}
}
}
// Use EDIT_CFG_LANGUAGES for the language list if it is set.
if let cfg_languages = env_opt("EDIT_CFG_LANGUAGES")
&& !cfg_languages.is_empty()
{
languages = cfg_languages.split(',').map(|lang| lang.to_string()).collect();
}
// Ensure English as the fallback language is always present.
if !languages.iter().any(|l| l == "en") {
languages.push("en".to_string());
}
// Normalize language tags for use in source code (i.e. no "-").
for lang in &mut languages {
if lang.is_empty() {
panic!("i18n: empty language tag");
}
for c in unsafe { lang.as_bytes_mut() } {
*c = match *c {
b'A'..=b'Z' | b'a'..=b'z' => c.to_ascii_lowercase(),
b'-' => b'_',
b'_' => b'_',
_ => panic!("i18n: language tag \"{lang}\" must be [a-zA-Z_-]"),
}
}
}
// * Validate that there are no duplicate language tags.
// * Validate that all language tags are valid.
// * Merge the aliases into the languages list.
let mut languages_with_aliases: Vec<_>;
{
let mut specified = HashSet::new();
for lang in &languages {
if !specified.insert(lang.as_str()) {
panic!("i18n: duplicate language tag \"{lang}\"");
}
}
let mut available = HashSet::new();
for v in translations.values() {
for lang in v.keys() {
available.insert(lang.as_str());
}
}
let mut invalid = Vec::new();
for lang in &languages {
if !available.contains(lang.as_str()) {
invalid.push(lang.as_str());
}
}
if !invalid.is_empty() {
panic!("i18n: invalid language tags {invalid:?}");
}
languages_with_aliases = languages.iter().map(|l| (l.clone(), l.clone())).collect();
for (alias, lang) in aliases {
if specified.contains(lang.as_str()) && !specified.contains(alias.as_str()) {
languages_with_aliases.push((alias, lang));
}
}
}
// Sort languages by:
// - "en" first, because it'll map to `LangId::en == 0`, which is the default.
// - then alphabetically
// - but tags with subtags (e.g. "zh_hans") before those without (e.g. "zh").
{
fn sort(a: &String, b: &String) -> std::cmp::Ordering {
match (a == "en", b == "en") {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => {
let (a0, a1) = a.split_once('_').unwrap_or((a, "xxxxxx"));
let (b0, b1) = b.split_once('_').unwrap_or((b, "xxxxxx"));
match a0.cmp(b0) {
std::cmp::Ordering::Equal => a1.cmp(b1),
ord => ord,
}
}
}
}
languages.sort_unstable_by(sort);
languages_with_aliases.sort_unstable_by(|a, b| sort(&a.0, &b.0));
}
let mut out = String::new();
// Generate the source code for the i18n data.
{
_ = write!(
out,
"\
// This file is generated by build.rs. Do not edit it manually.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LocId {{",
);
for (k, _) in translations.iter() {
_ = writeln!(out, " {k},");
}
_ = write!(
out,
"\
}}
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LangId {{
",
);
for lang in &languages {
_ = writeln!(out, " {lang},");
}
_ = write!(
out,
"\
}}
const LANGUAGES: &[(&str, LangId)] = &[
"
);
for (alias, lang) in &languages_with_aliases {
_ = writeln!(out, " ({alias:?}, LangId::{lang}),");
}
_ = write!(
out,
"\
];
const TRANSLATIONS: [[&str; {}]; {}] = [
",
translations.len(),
languages.len(),
);
for lang in &languages {
_ = writeln!(out, " [");
for (_, v) in translations.iter() {
const DEFAULT: &String = &String::new();
let v = v.get(lang).or_else(|| v.get("en")).unwrap_or(DEFAULT);
_ = writeln!(out, " {v:?},");
}
_ = writeln!(out, " ],");
}
_ = writeln!(out, "];");
}
out
}

128
crates/edit/build/main.rs Normal file
View File

@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(irrefutable_let_patterns)]
use crate::helpers::env_opt;
mod helpers;
mod i18n;
#[derive(Clone, Copy, PartialEq, Eq)]
enum TargetOs {
Windows,
MacOS,
Unix,
}
fn main() {
stdext::arena::init(128 * 1024 * 1024).unwrap();
let target_os = match env_opt("CARGO_CFG_TARGET_OS").as_str() {
"windows" => TargetOs::Windows,
"macos" | "ios" => TargetOs::MacOS,
_ => TargetOs::Unix,
};
compile_i18n();
configure_icu(target_os);
#[cfg(windows)]
configure_windows_binary(target_os);
}
fn compile_i18n() {
let i18n_path = "../../i18n/edit.toml";
let i18n = std::fs::read_to_string(i18n_path).unwrap();
let contents = i18n::generate(&i18n);
let out_dir = env_opt("OUT_DIR");
let path = format!("{out_dir}/i18n_edit.rs");
std::fs::write(&path, contents).unwrap();
println!("cargo::rerun-if-env-changed=EDIT_CFG_LANGUAGES");
println!("cargo::rerun-if-changed={i18n_path}");
}
fn configure_icu(target_os: TargetOs) {
let icuuc_soname = env_opt("EDIT_CFG_ICUUC_SONAME");
let icui18n_soname = env_opt("EDIT_CFG_ICUI18N_SONAME");
let cpp_exports = env_opt("EDIT_CFG_ICU_CPP_EXPORTS");
let renaming_version = env_opt("EDIT_CFG_ICU_RENAMING_VERSION");
let renaming_auto_detect = env_opt("EDIT_CFG_ICU_RENAMING_AUTO_DETECT");
// If none of the `EDIT_CFG_ICU*` environment variables are set,
// we default to enabling `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` on UNIX.
// This slightly improves portability at least in the cases where the SONAMEs match our defaults.
let renaming_auto_detect = if !renaming_auto_detect.is_empty() {
renaming_auto_detect.parse::<bool>().unwrap()
} else {
target_os == TargetOs::Unix
&& icuuc_soname.is_empty()
&& icui18n_soname.is_empty()
&& cpp_exports.is_empty()
&& renaming_version.is_empty()
};
if renaming_auto_detect && !renaming_version.is_empty() {
// It makes no sense to specify an explicit version and also ask for auto-detection.
panic!(
"Either `EDIT_CFG_ICU_RENAMING_AUTO_DETECT` or `EDIT_CFG_ICU_RENAMING_VERSION` must be set, but not both"
);
}
let icuuc_soname = if !icuuc_soname.is_empty() {
&icuuc_soname
} else {
match target_os {
TargetOs::Windows => "icuuc.dll",
TargetOs::MacOS => "libicucore.dylib",
TargetOs::Unix => "libicuuc.so",
}
};
let icui18n_soname = if !icui18n_soname.is_empty() {
&icui18n_soname
} else {
match target_os {
TargetOs::Windows => "icuin.dll",
TargetOs::MacOS => "libicucore.dylib",
TargetOs::Unix => "libicui18n.so",
}
};
let icu_export_prefix =
if !cpp_exports.is_empty() && cpp_exports.parse::<bool>().unwrap() { "_" } else { "" };
let icu_export_suffix =
if !renaming_version.is_empty() { format!("_{renaming_version}") } else { String::new() };
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUUC_SONAME");
println!("cargo::rustc-env=EDIT_CFG_ICUUC_SONAME={icuuc_soname}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICUI18N_SONAME");
println!("cargo::rustc-env=EDIT_CFG_ICUI18N_SONAME={icui18n_soname}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_PREFIX");
println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_PREFIX={icu_export_prefix}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_EXPORT_SUFFIX");
println!("cargo::rustc-env=EDIT_CFG_ICU_EXPORT_SUFFIX={icu_export_suffix}");
println!("cargo::rerun-if-env-changed=EDIT_CFG_ICU_RENAMING_AUTO_DETECT");
println!("cargo::rustc-check-cfg=cfg(edit_icu_renaming_auto_detect)");
if renaming_auto_detect {
println!("cargo::rustc-cfg=edit_icu_renaming_auto_detect");
}
}
#[cfg(windows)]
fn configure_windows_binary(target_os: TargetOs) {
if target_os != TargetOs::Windows {
return;
}
let manifest_path = "src/bin/edit/edit.exe.manifest";
let icon_path = "../../assets/edit.ico";
winresource::WindowsResource::new()
.set_manifest_file(manifest_path)
.set("FileDescription", "Microsoft Edit")
.set("LegalCopyright", "© Microsoft Corporation. All rights reserved.")
.set_icon(icon_path)
.compile()
.unwrap();
println!("cargo::rerun-if-changed={manifest_path}");
}

View File

@ -3,7 +3,7 @@
//! Base64 facilities.
use crate::arena::ArenaString;
use stdext::arena::ArenaString;
const CHARSET: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@ -79,8 +79,9 @@ pub fn encode(dst: &mut ArenaString, src: &[u8]) {
#[cfg(test)]
mod tests {
use stdext::arena::{Arena, ArenaString};
use super::encode;
use crate::arena::{Arena, ArenaString};
#[test]
fn test_basic() {

View File

@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::io;
use edit::{buffer, icu};
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Icu(icu::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Self::Io(err)
}
}
impl From<icu::Error> for Error {
fn from(err: icu::Error) -> Self {
Self::Icu(err)
}
}
impl From<buffer::IoError> for Error {
fn from(err: buffer::IoError) -> Self {
match err {
buffer::IoError::Io(e) => Self::Io(e),
buffer::IoError::Icu(e) => Self::Icu(e),
}
}
}

View File

@ -5,11 +5,13 @@ use std::collections::LinkedList;
use std::ffi::OsStr;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::{fs, io};
use edit::buffer::{RcTextBuffer, TextBuffer};
use edit::helpers::{CoordType, Point};
use edit::{apperr, path, sys};
use edit::{path, sys};
use crate::apperr;
use crate::state::DisplayablePathBuf;
pub struct Document {
@ -143,10 +145,10 @@ impl DocumentManager {
let (path, goto) = Self::parse_filename_goto(path);
let path = path::normalize(path);
let mut file = match Self::open_for_reading(&path) {
let mut file = match File::open(&path) {
Ok(file) => Some(file),
Err(err) if sys::apperr_is_not_found(err) => None,
Err(err) => return Err(err),
Err(err) if err.kind() == io::ErrorKind::NotFound => None,
Err(err) => return Err(err.into()),
};
let file_id = if file.is_some() { Some(sys::file_id(file.as_ref(), &path)?) } else { None };
@ -210,6 +212,16 @@ impl DocumentManager {
}
pub fn open_for_writing(path: &Path) -> apperr::Result<File> {
// Error handling for directory creation and file writing
// It is worth doing an existence check because it is significantly
// faster than calling mkdir() and letting it fail (at least on Windows).
if let Some(parent) = path.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)?;
}
File::create(path).map_err(apperr::Error::from)
}

View File

@ -38,7 +38,7 @@ pub fn draw_editor(ctx: &mut Context, state: &mut State) {
fn draw_search(ctx: &mut Context, state: &mut State) {
if let Err(err) = icu::init() {
error_log_add(ctx, state, err);
error_log_add(ctx, state, err.into());
state.wants_search.kind = StateSearchKind::Disabled;
return;
}
@ -181,12 +181,12 @@ pub fn search_execute(ctx: &mut Context, state: &mut State, action: SearchAction
SearchAction::Replace => doc.buffer.borrow_mut().find_and_replace(
&state.search_needle,
state.search_options,
&state.search_replacement,
state.search_replacement.as_bytes(),
),
SearchAction::ReplaceAll => doc.buffer.borrow_mut().find_and_replace_all(
&state.search_needle,
state.search_options,
&state.search_replacement,
state.search_replacement.as_bytes(),
),
}
.is_ok();

View File

@ -5,12 +5,12 @@ use std::cmp::Ordering;
use std::fs;
use std::path::{Path, PathBuf};
use edit::arena::scratch_arena;
use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::input::{kbmod, vk};
use edit::tui::*;
use edit::{icu, path};
use stdext::arena::scratch_arena;
use crate::localization::*;
use crate::state::*;
@ -341,7 +341,7 @@ fn draw_dialog_saveas_refresh_files(state: &mut State) {
}
for entries in &mut dirs_files[1..] {
entries.sort_by(|a, b| {
entries.sort_unstable_by(|a, b| {
let a = a.as_bytes();
let b = b.as_bytes();

View File

@ -1,10 +1,10 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use edit::arena_format;
use edit::helpers::*;
use edit::input::{kbmod, vk};
use edit::tui::*;
use stdext::arena_format;
use crate::localization::*;
use crate::state::*;

View File

@ -1,13 +1,14 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use edit::arena::scratch_arena;
use edit::framebuffer::{Attributes, IndexedColor};
use edit::fuzzy::score_fuzzy;
use edit::helpers::*;
use edit::icu;
use edit::input::vk;
use edit::tui::*;
use edit::{arena_format, icu};
use stdext::arena::scratch_arena;
use stdext::arena_format;
use crate::localization::*;
use crate::state::*;
@ -301,7 +302,7 @@ fn encoding_picker_update_list(state: &mut State) {
}
}
matches.sort_by(|a, b| b.0.cmp(&a.0));
matches.sort_unstable_by_key(|b| std::cmp::Reverse(b.0));
state.encoding_picker_results = Some(Vec::from_iter(matches.iter().map(|(_, enc)| *enc)));
}

View File

@ -2,7 +2,6 @@
<assembly
xmlns="urn:schemas-microsoft-com:asm.v1"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"
xmlns:cv1="urn:schemas-microsoft-com:compatibility.v1"
xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings"
xmlns:ws3="http://schemas.microsoft.com/SMI/2019/WindowsSettings"
xmlns:ws4="http://schemas.microsoft.com/SMI/2020/WindowsSettings"
@ -14,9 +13,9 @@
<ws4:heapType>SegmentHeap</ws4:heapType>
</windowsSettings>
</asmv3:application>
<cv1:compatibility>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</cv1:compatibility>
</compatibility>
</assembly>

View File

@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use edit::sys;
use stdext::AsciiStringHelpers as _;
use stdext::arena::scratch_arena;
include!(concat!(env!("OUT_DIR"), "/i18n_edit.rs"));
static mut S_LANG: LangId = LangId::en;
pub fn init() {
let scratch = scratch_arena(None);
let langs = sys::preferred_languages(&scratch);
let mut lang = LangId::en;
'outer: for l in langs {
for (prefix, id) in LANGUAGES {
if l.starts_with_ignore_ascii_case(prefix) {
lang = *id;
break 'outer;
}
}
}
unsafe {
S_LANG = lang;
}
}
pub fn loc(id: LocId) -> &'static str {
TRANSLATIONS[unsafe { S_LANG as usize }][id as usize]
}

View File

@ -1,8 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![feature(allocator_api, let_chains, linked_list_cursors, string_from_utf8_lossy_owned)]
#![feature(allocator_api, linked_list_cursors, string_from_utf8_lossy_owned)]
mod apperr;
mod documents;
mod draw_editor;
mod draw_filepicker;
@ -12,8 +13,6 @@ mod localization;
mod state;
use std::borrow::Cow;
#[cfg(feature = "debug-latency")]
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{env, process};
@ -22,22 +21,26 @@ use draw_editor::*;
use draw_filepicker::*;
use draw_menubar::*;
use draw_statusbar::*;
use edit::arena::{self, Arena, ArenaString, scratch_arena};
use edit::framebuffer::{self, IndexedColor};
use edit::helpers::{CoordType, KIBI, MEBI, MetricFormatter, Rect, Size};
use edit::helpers::*;
use edit::input::{self, kbmod, vk};
use edit::oklab::oklab_blend;
use edit::oklab::StraightRgba;
use edit::tui::*;
use edit::vt::{self, Token};
use edit::{apperr, arena_format, base64, path, sys, unicode};
use edit::{base64, path, sys, unicode};
use localization::*;
use state::*;
use stdext::arena::{self, Arena, ArenaString, scratch_arena};
use stdext::arena_format;
#[cfg(target_pointer_width = "32")]
const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI;
#[cfg(target_pointer_width = "64")]
const SCRATCH_ARENA_CAPACITY: usize = 512 * MEBI;
// NOTE: Before our main() gets called, Rust initializes its stdlib. This pulls in the entire
// std::io::{stdin, stdout, stderr} machinery, and probably some more, which amounts to about 20KB.
// It can technically be avoided nowadays with `#![no_main]`. Maybe a fun project for later? :)
fn main() -> process::ExitCode {
if cfg!(debug_assertions) {
let hook = std::panic::take_hook();
@ -51,7 +54,7 @@ fn main() -> process::ExitCode {
match run() {
Ok(()) => process::ExitCode::SUCCESS,
Err(err) => {
sys::write_stdout(&format!("{}\r\n", FormatApperr::from(err)));
sys::write_stdout(&format!("{}\n", FormatApperr::from(err)));
process::ExitCode::FAILURE
}
}
@ -59,7 +62,7 @@ fn main() -> process::ExitCode {
fn run() -> apperr::Result<()> {
// Init `sys` first, as everything else may depend on its functionality (IO, function pointers, etc.).
let _sys_deinit = sys::init()?;
let _sys_deinit = sys::init();
// Next init `arena`, so that `scratch_arena` works. `loc` depends on it.
arena::init(SCRATCH_ARENA_CAPACITY)?;
// Init the `loc` module, so that error messages are localized.
@ -70,10 +73,11 @@ fn run() -> apperr::Result<()> {
return Ok(());
}
// sys::init() will switch the terminal to raw mode which prevents the user from pressing Ctrl+C.
// Since the `read_file` call may hang for some reason, we must only call this afterwards.
// `set_modes()` will enable mouse mode which is equally annoying to switch out for users
// and so we do it afterwards, for similar reasons.
// This will reopen stdin if it's redirected (which may fail) and switch
// the terminal to raw mode which prevents the user from pressing Ctrl+C.
// `handle_args` may want to print a help message (must not fail),
// and reads files (may hang; should be cancelable with Ctrl+C).
// As such, we call this after `handle_args`.
sys::switch_modes()?;
let mut vt_parser = vt::Parser::new();
@ -82,15 +86,15 @@ fn run() -> apperr::Result<()> {
let _restore = setup_terminal(&mut tui, &mut state, &mut vt_parser);
state.menubar_color_bg = oklab_blend(
tui.indexed(IndexedColor::Background),
tui.indexed_alpha(IndexedColor::BrightBlue, 1, 2),
);
state.menubar_color_bg = tui.indexed(IndexedColor::Background).oklab_blend(tui.indexed_alpha(
IndexedColor::BrightBlue,
1,
2,
));
state.menubar_color_fg = tui.contrasted(state.menubar_color_bg);
let floater_bg = oklab_blend(
tui.indexed_alpha(IndexedColor::Background, 2, 3),
tui.indexed_alpha(IndexedColor::Foreground, 1, 3),
);
let floater_bg = tui
.indexed_alpha(IndexedColor::Background, 2, 3)
.oklab_blend(tui.indexed_alpha(IndexedColor::Foreground, 1, 3));
let floater_fg = tui.contrasted(floater_bg);
tui.setup_modifier_translations(ModifierTranslations {
ctrl: loc(LocId::Ctrl),
@ -168,13 +172,7 @@ fn run() -> apperr::Result<()> {
let scratch = scratch_arena(None);
let mut output = tui.render(&scratch);
{
let filename = state.documents.active().map_or("", |d| &d.filename);
if filename != state.osc_title_filename {
write_terminal_title(&mut output, filename);
state.osc_title_filename = filename.to_string();
}
}
write_terminal_title(&mut output, &mut state);
if state.osc_clipboard_sync {
write_osc_clipboard(&mut tui, &mut state, &mut output);
@ -182,6 +180,8 @@ fn run() -> apperr::Result<()> {
#[cfg(feature = "debug-latency")]
{
use std::fmt::Write as _;
// Print the number of passes and latency in the top right corner.
let time_end = std::time::Instant::now();
let status = time_end - time_beg;
@ -230,23 +230,37 @@ fn run() -> apperr::Result<()> {
fn handle_args(state: &mut State) -> apperr::Result<bool> {
let scratch = scratch_arena(None);
let mut paths: Vec<PathBuf, &Arena> = Vec::new_in(&*scratch);
let mut cwd = env::current_dir()?;
let cwd = env::current_dir()?;
let mut dir = None;
let mut parse_args = true;
// The best CLI argument parser in the world.
for arg in env::args_os().skip(1) {
if arg == "-h" || arg == "--help" || (cfg!(windows) && arg == "/?") {
print_help();
return Ok(true);
} else if arg == "-v" || arg == "--version" {
print_version();
return Ok(true);
} else if arg == "-" {
paths.clear();
break;
if parse_args {
if arg == "--" {
parse_args = false;
continue;
}
if arg == "-" {
paths.clear();
break;
}
if arg == "-h" || arg == "--help" || (cfg!(windows) && arg == "/?") {
print_help();
return Ok(true);
}
if arg == "-v" || arg == "--version" {
print_version();
return Ok(true);
}
}
let p = cwd.join(Path::new(&arg));
let p = path::normalize(&p);
if !p.is_dir() {
if p.is_dir() {
state.wants_file_picker = StateFilePicker::Open;
dir = Some(p);
} else {
paths.push(p);
}
}
@ -254,9 +268,6 @@ fn handle_args(state: &mut State) -> apperr::Result<bool> {
for p in &paths {
state.documents.add_file_path(p)?;
}
if let Some(parent) = paths.first().and_then(|p| p.parent()) {
cwd = parent.to_path_buf();
}
if let Some(mut file) = sys::open_stdin_if_redirected() {
let doc = state.documents.add_untitled()?;
@ -268,24 +279,30 @@ fn handle_args(state: &mut State) -> apperr::Result<bool> {
state.documents.add_untitled()?;
}
state.file_picker_pending_dir = DisplayablePathBuf::from_path(cwd);
if dir.is_none()
&& let Some(parent) = paths.last().and_then(|p| p.parent())
{
dir = Some(parent.to_path_buf());
}
state.file_picker_pending_dir = DisplayablePathBuf::from_path(dir.unwrap_or(cwd));
Ok(false)
}
fn print_help() {
sys::write_stdout(concat!(
"Usage: edit [OPTIONS] [FILE[:LINE[:COLUMN]]]\r\n",
"Options:\r\n",
" -h, --help Print this help message\r\n",
" -v, --version Print the version number\r\n",
"\r\n",
"Arguments:\r\n",
" FILE[:LINE[:COLUMN]] The file to open, optionally with line and column (e.g., foo.txt:123:45)\r\n",
"Usage: edit [OPTIONS] [FILE[:LINE[:COLUMN]]]\n",
"Options:\n",
" -h, --help Print this help message\n",
" -v, --version Print the version number\n",
"\n",
"Arguments:\n",
" FILE[:LINE[:COLUMN]] The file to open, optionally with line and column (e.g., foo.txt:123:45)\n",
));
}
fn print_version() {
sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\r\n"));
sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\n"));
}
fn draw(ctx: &mut Context, state: &mut State) {
@ -377,16 +394,30 @@ fn draw_handle_wants_exit(_ctx: &mut Context, state: &mut State) {
}
}
#[cold]
fn write_terminal_title(output: &mut ArenaString, filename: &str) {
output.push_str("\x1b]0;");
fn write_terminal_title(output: &mut ArenaString, state: &mut State) {
let (filename, dirty) = state
.documents
.active()
.map_or(("", false), |d| (&d.filename, d.buffer.borrow().is_dirty()));
if filename == state.osc_title_file_status.filename
&& dirty == state.osc_title_file_status.dirty
{
return;
}
output.push_str("\x1b]0;");
if !filename.is_empty() {
if dirty {
output.push_str("");
}
output.push_str(&sanitize_control_chars(filename));
output.push_str(" - ");
}
output.push_str("edit\x1b\\");
state.osc_title_file_status.filename = filename.to_string();
state.osc_title_file_status.dirty = dirty;
}
const LARGE_CLIPBOARD_THRESHOLD: usize = 128 * KIBI;
@ -611,7 +642,7 @@ fn setup_terminal(tui: &mut Tui, state: &mut State, vt_parser: &mut vt::Parser)
}
}
*color = rgb | 0xff000000;
*color = StraightRgba::from_le(rgb | 0xff000000);
color_responses += 1;
osc_buffer.clear();
}

View File

@ -8,9 +8,11 @@ use std::path::{Path, PathBuf};
use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::oklab::StraightRgba;
use edit::tui::*;
use edit::{apperr, buffer, icu, sys};
use edit::{buffer, icu};
use crate::apperr;
use crate::documents::DocumentManager;
use crate::localization::*;
@ -26,10 +28,9 @@ impl From<apperr::Error> for FormatApperr {
impl std::fmt::Display for FormatApperr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.0 {
apperr::APP_ICU_MISSING => f.write_str(loc(LocId::ErrorIcuMissing)),
apperr::Error::App(code) => write!(f, "Unknown app error code: {code}"),
apperr::Error::Icu(code) => icu::apperr_format(f, code),
apperr::Error::Sys(code) => sys::apperr_format(f, code),
apperr::Error::Icu(icu::ICU_MISSING_ERROR) => f.write_str(loc(LocId::ErrorIcuMissing)),
apperr::Error::Icu(ref err) => err.fmt(f),
apperr::Error::Io(ref err) => err.fmt(f),
}
}
}
@ -120,9 +121,15 @@ pub enum StateEncodingChange {
Reopen,
}
#[derive(Default)]
pub struct OscTitleFileStatus {
pub filename: String,
pub dirty: bool,
}
pub struct State {
pub menubar_color_bg: u32,
pub menubar_color_fg: u32,
pub menubar_color_bg: StraightRgba,
pub menubar_color_fg: StraightRgba,
pub documents: DocumentManager,
@ -161,7 +168,7 @@ pub struct State {
pub goto_target: String,
pub goto_invalid: bool,
pub osc_title_filename: String,
pub osc_title_file_status: OscTitleFileStatus,
pub osc_clipboard_sync: bool,
pub osc_clipboard_always_send: bool,
pub exit: bool,
@ -170,8 +177,8 @@ pub struct State {
impl State {
pub fn new() -> apperr::Result<Self> {
Ok(Self {
menubar_color_bg: 0,
menubar_color_fg: 0,
menubar_color_bg: StraightRgba::zero(),
menubar_color_fg: StraightRgba::zero(),
documents: Default::default(),
@ -209,7 +216,7 @@ impl State {
goto_target: Default::default(),
goto_invalid: false,
osc_title_filename: Default::default(),
osc_title_file_status: Default::default(),
osc_clipboard_sync: false,
osc_clipboard_always_send: false,
exit: false,

View File

@ -3,11 +3,13 @@
use std::ops::Range;
use std::ptr::{self, NonNull};
use std::slice;
use std::{io, slice};
use stdext::sys::{virtual_commit, virtual_release, virtual_reserve};
use stdext::{ReplaceRange as _, slice_copy_safe};
use crate::document::{ReadableDocument, WriteableDocument};
use crate::helpers::*;
use crate::{apperr, sys};
#[cfg(target_pointer_width = "32")]
const LARGE_CAPACITY: usize = 128 * MEBI;
@ -31,7 +33,7 @@ impl Drop for BackingBuffer {
fn drop(&mut self) {
unsafe {
if let Self::VirtualMemory(ptr, reserve) = *self {
sys::virtual_release(ptr, reserve);
virtual_release(ptr, reserve);
}
}
}
@ -62,7 +64,7 @@ pub struct GapBuffer {
}
impl GapBuffer {
pub fn new(small: bool) -> apperr::Result<Self> {
pub fn new(small: bool) -> io::Result<Self> {
let reserve;
let buffer;
let text;
@ -73,7 +75,7 @@ impl GapBuffer {
buffer = BackingBuffer::Vec(Vec::new());
} else {
reserve = LARGE_CAPACITY;
text = unsafe { sys::virtual_reserve(reserve)? };
text = unsafe { virtual_reserve(reserve)? };
buffer = BackingBuffer::VirtualMemory(text, reserve);
}
@ -195,7 +197,7 @@ impl GapBuffer {
match &mut self.buffer {
BackingBuffer::VirtualMemory(ptr, _) => unsafe {
if sys::virtual_commit(ptr.add(bytes_old), bytes_new - bytes_old).is_err() {
if virtual_commit(ptr.add(bytes_old), bytes_new - bytes_old).is_err() {
return;
}
},

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Clipboard facilities for the editor.
/// The builtin, internal clipboard of the editor.

View File

@ -8,8 +8,8 @@ use std::mem;
use std::ops::Range;
use std::path::PathBuf;
use crate::arena::{ArenaString, scratch_arena};
use crate::helpers::ReplaceRange as _;
use stdext::ReplaceRange as _;
use stdext::arena::{ArenaString, scratch_arena};
/// An abstraction over reading from text containers.
pub trait ReadableDocument {

View File

@ -9,9 +9,10 @@ use std::ops::{BitOr, BitXor};
use std::ptr;
use std::slice::ChunksExact;
use crate::arena::{Arena, ArenaString};
use stdext::arena::{Arena, ArenaString};
use crate::helpers::{CoordType, Point, Rect, Size};
use crate::oklab::{oklab_blend, srgb_to_oklab};
use crate::oklab::StraightRgba;
use crate::simd::{MemsetSafe, memset};
use crate::unicode::MeasurementConfig;
@ -53,30 +54,36 @@ pub enum IndexedColor {
Foreground,
}
impl<T: Into<u8>> From<T> for IndexedColor {
fn from(value: T) -> Self {
unsafe { std::mem::transmute(value.into() & 0xF) }
}
}
/// Number of indices used by [`IndexedColor`].
pub const INDEXED_COLORS_COUNT: usize = 18;
/// Fallback theme. Matches Windows Terminal's Ottosson theme.
pub const DEFAULT_THEME: [u32; INDEXED_COLORS_COUNT] = [
0xff000000, // Black
0xff212cbe, // Red
0xff3aae3f, // Green
0xff4a9abe, // Yellow
0xffbe4d20, // Blue
0xffbe54bb, // Magenta
0xffb2a700, // Cyan
0xffbebebe, // White
0xff808080, // BrightBlack
0xff303eff, // BrightRed
0xff51ea58, // BrightGreen
0xff44c9ff, // BrightYellow
0xffff6a2f, // BrightBlue
0xffff74fc, // BrightMagenta
0xfff0e100, // BrightCyan
0xffffffff, // BrightWhite
pub const DEFAULT_THEME: [StraightRgba; INDEXED_COLORS_COUNT] = [
StraightRgba::from_be(0x000000ff), // Black
StraightRgba::from_be(0xbe2c21ff), // Red
StraightRgba::from_be(0x3fae3aff), // Green
StraightRgba::from_be(0xbe9a4aff), // Yellow
StraightRgba::from_be(0x204dbeff), // Blue
StraightRgba::from_be(0xbb54beff), // Magenta
StraightRgba::from_be(0x00a7b2ff), // Cyan
StraightRgba::from_be(0xbebebeff), // White
StraightRgba::from_be(0x808080ff), // BrightBlack
StraightRgba::from_be(0xff3e30ff), // BrightRed
StraightRgba::from_be(0x58ea51ff), // BrightGreen
StraightRgba::from_be(0xffc944ff), // BrightYellow
StraightRgba::from_be(0x2f6affff), // BrightBlue
StraightRgba::from_be(0xfc74ffff), // BrightMagenta
StraightRgba::from_be(0x00e1f0ff), // BrightCyan
StraightRgba::from_be(0xffffffff), // BrightWhite
// --------
0xff000000, // Background
0xffbebebe, // Foreground
StraightRgba::from_be(0x000000ff), // Background
StraightRgba::from_be(0xbebebeff), // Foreground
];
/// A shoddy framebuffer for terminal applications.
@ -91,7 +98,7 @@ pub const DEFAULT_THEME: [u32; INDEXED_COLORS_COUNT] = [
/// the screen all the time.
pub struct Framebuffer {
/// Store the color palette.
indexed_colors: [u32; INDEXED_COLORS_COUNT],
indexed_colors: [StraightRgba; INDEXED_COLORS_COUNT],
/// Front and back buffers. Indexed by `frame_counter & 1`.
buffers: [Buffer; 2],
/// The current frame counter. Increments on every `flip` call.
@ -99,12 +106,14 @@ pub struct Framebuffer {
/// The colors used for `contrast()`. It stores the default colors
/// of the palette as [dark, light], unless the palette is recognized
/// as a light them, in which case it swaps them.
auto_colors: [u32; 2],
auto_colors: [StraightRgba; 2],
/// Above this lightness value, we consider a color to be "light".
auto_color_threshold: f32,
/// A cache table for previously contrasted colors.
/// See: <https://fgiesen.wordpress.com/2019/02/11/cache-tables/>
contrast_colors: [Cell<(u32, u32)>; CACHE_TABLE_SIZE],
background_fill: u32,
foreground_fill: u32,
contrast_colors: [Cell<(StraightRgba, StraightRgba)>; CACHE_TABLE_SIZE],
background_fill: StraightRgba,
foreground_fill: StraightRgba,
}
impl Framebuffer {
@ -118,7 +127,9 @@ impl Framebuffer {
DEFAULT_THEME[IndexedColor::Black as usize],
DEFAULT_THEME[IndexedColor::BrightWhite as usize],
],
contrast_colors: [const { Cell::new((0, 0)) }; CACHE_TABLE_SIZE],
auto_color_threshold: 0.5,
contrast_colors: [const { Cell::new((StraightRgba::zero(), StraightRgba::zero())) };
CACHE_TABLE_SIZE],
background_fill: DEFAULT_THEME[IndexedColor::Background as usize],
foreground_fill: DEFAULT_THEME[IndexedColor::Foreground as usize],
}
@ -128,16 +139,26 @@ impl Framebuffer {
///
/// If you call this method, [`Framebuffer`] expects that you
/// successfully detect the light/dark mode of the terminal.
pub fn set_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
pub fn set_indexed_colors(&mut self, colors: [StraightRgba; INDEXED_COLORS_COUNT]) {
self.indexed_colors = colors;
self.background_fill = 0;
self.foreground_fill = 0;
self.background_fill = StraightRgba::zero();
self.foreground_fill = StraightRgba::zero();
self.auto_colors = [
self.indexed_colors[IndexedColor::Black as usize],
self.indexed_colors[IndexedColor::BrightWhite as usize],
];
if !Self::is_dark(self.auto_colors[0]) {
// It's not guaranteed that Black is actually dark and BrightWhite light (vice versa for a light theme).
// Such is the case with macOS 26's "Clear Dark" theme (and probably a lot other themes).
// Its black is #35424C (l=0.3716; oof!) and bright white is #E5EFF5 (l=0.9464).
// If we have a color such as #43698A (l=0.5065), which is l>0.5 ("light") and need a contrasting color,
// we need that to be #E5EFF5, even though that's also l>0.5. With a midpoint of 0.659, we get that right.
let lightness = self.auto_colors.map(|c| c.as_oklab().lightness());
self.auto_color_threshold = (lightness[0] + lightness[1]) * 0.5;
// Ensure [0] is dark and [1] is light.
if lightness[0] > lightness[1] {
self.auto_colors.swap(0, 1);
}
}
@ -154,7 +175,7 @@ impl Framebuffer {
let front = &mut self.buffers[self.frame_counter & 1];
// Trigger a full redraw. (Yes, it's a hack.)
front.fg_bitmap.fill(1);
front.fg_bitmap.fill(StraightRgba::from_le(1));
// Trigger a cursor update as well, just to be sure.
front.cursor = Cursor::new_invalid();
}
@ -308,7 +329,7 @@ impl Framebuffer {
}
#[inline]
pub fn indexed(&self, index: IndexedColor) -> u32 {
pub fn indexed(&self, index: IndexedColor) -> StraightRgba {
self.indexed_colors[index as usize]
}
@ -317,39 +338,38 @@ impl Framebuffer {
/// To facilitate constant folding by the compiler,
/// alpha is given as a fraction (`numerator` / `denominator`).
#[inline]
pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
let c = self.indexed_colors[index as usize];
pub fn indexed_alpha(
&self,
index: IndexedColor,
numerator: u32,
denominator: u32,
) -> StraightRgba {
let c = self.indexed_colors[index as usize].to_le();
let a = 255 * numerator / denominator;
let r = (((c >> 16) & 0xFF) * numerator) / denominator;
let g = (((c >> 8) & 0xFF) * numerator) / denominator;
let b = ((c & 0xFF) * numerator) / denominator;
a << 24 | r << 16 | g << 8 | b
StraightRgba::from_le(a << 24 | (c & 0x00ffffff))
}
/// Returns a color opposite to the brightness of the given `color`.
pub fn contrasted(&self, color: u32) -> u32 {
let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
pub fn contrasted(&self, color: StraightRgba) -> StraightRgba {
let idx = (color.to_ne() as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
let slot = self.contrast_colors[idx].get();
if slot.0 == color { slot.1 } else { self.contrasted_slow(color) }
}
#[cold]
fn contrasted_slow(&self, color: u32) -> u32 {
let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
let contrast = self.auto_colors[Self::is_dark(color) as usize];
fn contrasted_slow(&self, color: StraightRgba) -> StraightRgba {
let idx = (color.to_ne() as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
let is_dark = color.as_oklab().lightness() < self.auto_color_threshold;
let contrast = self.auto_colors[is_dark as usize];
self.contrast_colors[idx].set((color, contrast));
contrast
}
fn is_dark(color: u32) -> bool {
srgb_to_oklab(color).l < 0.5
}
/// Blends the given sRGB color onto the background bitmap.
///
/// TODO: The current approach blends foreground/background independently,
/// but ideally `blend_bg` with semi-transparent dark should also darken text below it.
pub fn blend_bg(&mut self, target: Rect, bg: u32) {
pub fn blend_bg(&mut self, target: Rect, bg: StraightRgba) {
let back = &mut self.buffers[self.frame_counter & 1];
back.bg_bitmap.blend(target, bg);
}
@ -358,7 +378,7 @@ impl Framebuffer {
///
/// TODO: The current approach blends foreground/background independently,
/// but ideally `blend_fg` should blend with the background color below it.
pub fn blend_fg(&mut self, target: Rect, fg: u32) {
pub fn blend_fg(&mut self, target: Rect, fg: StraightRgba) {
let back = &mut self.buffers[self.frame_counter & 1];
back.fg_bitmap.blend(target, fg);
}
@ -476,13 +496,13 @@ impl Framebuffer {
&& back_attr[chunk_end] == attr
} {}
if last_bg != bg as u64 {
last_bg = bg as u64;
if last_bg != bg.to_ne() as u64 {
last_bg = bg.to_ne() as u64;
self.format_color(&mut result, false, bg);
}
if last_fg != fg as u64 {
last_fg = fg as u64;
if last_fg != fg.to_ne() as u64 {
last_fg = fg.to_ne() as u64;
self.format_color(&mut result, true, fg);
}
@ -537,7 +557,7 @@ impl Framebuffer {
result
}
fn format_color(&self, dst: &mut ArenaString, fg: bool, mut color: u32) {
fn format_color(&self, dst: &mut ArenaString, fg: bool, mut color: StraightRgba) {
let typ = if fg { '3' } else { '4' };
// Some terminals support transparent backgrounds which are used
@ -553,20 +573,20 @@ impl Framebuffer {
// the output slightly and ensures that we keep "default foreground"
// and "color that happens to be default foreground" separate.
// (This also applies to the background color by the way.)
if color == 0 {
if color.to_ne() == 0 {
_ = write!(dst, "\x1b[{typ}9m");
return;
}
if (color & 0xff000000) != 0xff000000 {
if color.alpha() != 0xff {
let idx = if fg { IndexedColor::Foreground } else { IndexedColor::Background };
let dst = self.indexed(idx);
color = oklab_blend(dst, color);
color = dst.oklab_blend(color);
}
let r = color & 0xff;
let g = (color >> 8) & 0xff;
let b = (color >> 16) & 0xff;
let r = color.red();
let g = color.green();
let b = color.blue();
_ = write!(dst, "\x1b[{typ}8;2;{r};{g};{b}m");
}
}
@ -730,16 +750,16 @@ impl LineBuffer {
/// An sRGB bitmap.
#[derive(Default)]
struct Bitmap {
data: Vec<u32>,
data: Vec<StraightRgba>,
size: Size,
}
impl Bitmap {
fn new(size: Size) -> Self {
Self { data: vec![0; (size.width * size.height) as usize], size }
Self { data: vec![StraightRgba::zero(); (size.width * size.height) as usize], size }
}
fn fill(&mut self, color: u32) {
fn fill(&mut self, color: StraightRgba) {
memset(&mut self.data, color);
}
@ -747,8 +767,8 @@ impl Bitmap {
///
/// This uses the `oklab` color space for blending so the
/// resulting colors may look different from what you'd expect.
fn blend(&mut self, target: Rect, color: u32) {
if (color & 0xff000000) == 0x00000000 {
fn blend(&mut self, target: Rect, color: StraightRgba) {
if color.alpha() == 0 {
return;
}
@ -768,7 +788,7 @@ impl Bitmap {
let end = y * stride + right;
let data = &mut self.data[beg..end];
if (color & 0xff000000) == 0xff000000 {
if color.alpha() == 0xff {
memset(data, color);
} else {
let end = data.len();
@ -785,7 +805,7 @@ impl Bitmap {
} {}
let chunk_end = off;
let c = oklab_blend(c, color);
let c = c.oklab_blend(color);
memset(&mut data[chunk_beg..chunk_end], c);
off < end
@ -795,7 +815,7 @@ impl Bitmap {
}
/// Iterates over each row in the bitmap.
fn iter(&self) -> ChunksExact<'_, u32> {
fn iter(&self) -> ChunksExact<'_, StraightRgba> {
self.data.chunks_exact(self.size.width as usize)
}
}

View File

@ -7,7 +7,8 @@
use std::vec;
use crate::arena::{Arena, scratch_arena};
use stdext::arena::{Arena, scratch_arena};
use crate::icu;
const NO_MATCH: i32 = 0;

273
crates/edit/src/glob.rs Normal file
View File

@ -0,0 +1,273 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Simple glob matching.
//!
//! Supported patterns:
//! - `*` matches any characters except for path separators, including an empty string.
//! - `**` matches any characters, including an empty string.
//! For convenience, `/**/` also matches `/`.
use std::path::is_separator;
#[inline]
pub fn glob_match<P: AsRef<[u8]>, N: AsRef<[u8]>>(pattern: P, name: N) -> bool {
glob(pattern.as_ref(), name.as_ref())
}
fn glob(pattern: &[u8], name: &[u8]) -> bool {
fast_path(pattern, name).unwrap_or_else(|| slow_path(pattern, name))
}
// Fast-pass for the most common patterns:
// * Matching files by extension (e.g., **/*.rs)
// * Matching files by name (e.g., **/Cargo.toml)
fn fast_path(pattern: &[u8], name: &[u8]) -> Option<bool> {
// In either case, the glob must start with "**/".
let mut suffix = pattern.strip_prefix(b"**/")?;
if suffix.is_empty() {
return None;
}
// Determine whether it's "**/" or "**/*".
let mut needs_dir_anchor = true;
if let Some(s) = suffix.strip_prefix(b"*") {
suffix = s;
needs_dir_anchor = false;
}
// Restrict down to anything we can handle with a suffix check.
if suffix.is_empty() || contains_magic(suffix) {
return None;
}
Some(
match_path_suffix(name, suffix)
&& (
// In case of "**/*extension" a simple suffix match is sufficient.
!needs_dir_anchor
// But for "**/filename" we need to ensure that path is either "filename"...
|| name.len() == suffix.len()
// ...or that it is ".../filename".
|| is_separator(name[name.len() - suffix.len() - 1] as char)
),
)
}
fn contains_magic(pattern: &[u8]) -> bool {
pattern.contains(&b'*')
}
fn match_path_suffix(path: &[u8], suffix: &[u8]) -> bool {
if path.len() < suffix.len() {
return false;
}
let path = &path[path.len() - suffix.len()..];
#[cfg(windows)]
{
path.iter().zip(suffix.iter()).all(|(a, b)| {
let a = if *a == b'\\' { b'/' } else { *a };
let b = if *b == b'\\' { b'/' } else { *b };
a.eq_ignore_ascii_case(&b)
})
}
#[cfg(not(windows))]
path.eq_ignore_ascii_case(suffix)
}
// This code is based on https://research.swtch.com/glob.go
// It's not particularly fast, but it doesn't need to be. It doesn't run often.
#[cold]
fn slow_path(pattern: &[u8], name: &[u8]) -> bool {
let mut px = 0;
let mut nx = 0;
let mut next_px = 0;
let mut next_nx = 0;
let mut is_double_star = false;
while px < pattern.len() || nx < name.len() {
if px < pattern.len() {
match pattern[px] {
b'*' => {
// Try to match at nx. If that doesn't work out, restart at nx+1 next.
next_px = px;
next_nx = nx + 1;
px += 1;
is_double_star = false;
if px < pattern.len() && pattern[px] == b'*' {
px += 1;
is_double_star = true;
// For convenience, /**/ also matches /
if px >= 3
&& px < pattern.len()
&& pattern[px] == b'/'
&& pattern[px - 3] == b'/'
{
px += 1;
}
}
continue;
}
c => {
if nx < name.len() && name[nx].eq_ignore_ascii_case(&c) {
px += 1;
nx += 1;
continue;
}
}
}
}
// Mismatch. Maybe restart.
if next_nx > 0
&& next_nx <= name.len()
&& (is_double_star || !is_separator(name[next_nx - 1] as char))
{
px = next_px;
nx = next_nx;
continue;
}
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_glob_match() {
let tests = [
// Test cases from https://research.swtch.com/glob.go
("", "", true),
("x", "", false),
("", "x", false),
("abc", "abc", true),
("*", "abc", true),
("*c", "abc", true),
("*b", "abc", false),
("a*", "abc", true),
("b*", "abc", false),
("a*", "a", true),
("*a", "a", true),
("a*b*c*d*e*", "axbxcxdxe", true),
("a*b*c*d*e*", "axbxcxdxexxx", true),
("*x", "xxx", true),
// Test cases from https://github.com/golang/go/blob/master/src/path/filepath/match_test.go
("a*", "ab/c", false),
("a*b", "a/b", false),
("a*/b", "abc/b", true),
("a*/b", "a/c/b", false),
("a*b*c*d*e*/f", "axbxcxdxe/f", true),
("a*b*c*d*e*/f", "axbxcxdxexxx/f", true),
("a*b*c*d*e*/f", "axbxcxdxe/xxx/f", false),
("a*b*c*d*e*/f", "axbxcxdxexxx/fff", false),
// Single star (*)
// - Empty string
("*", "", true),
// - Anything else is covered above
// Double star (**)
// - Empty string
("**", "", true),
("a**", "a", true),
("**a", "a", true),
// - Prefix
("**", "abc", true),
("**", "foo/baz/bar", true),
("**c", "abc", true),
("**b", "abc", false),
// - Infix
("a**c", "ac", true),
("a**c", "abc", true),
("a**c", "abd", false),
("a**d", "abc", false),
("a**c", "a/bc", true),
("a**c", "ab/c", true),
("a**c", "a/b/c", true),
// -- Infix with left separator
("a/**c", "ac", false),
("a/**c", "a/c", true),
("a/**c", "b/c", false),
("a/**c", "a/d", false),
("a/**c", "a/b/c", true),
("a/**c", "a/b/d", false),
("a/**c", "d/b/c", false),
// -- Infix with right separator
("a**/c", "ac", false),
("a**/c", "a/c", true),
("a**/c", "b/c", false),
("a**/c", "a/d", false),
("a**/c", "a/b/c", true),
("a**/c", "a/b/d", false),
("a**/c", "d/b/c", false),
// - Infix with two separators
("a/**/c", "ac", false),
("a/**/c", "a/c", true),
("a/**/c", "b/c", false),
("a/**/c", "a/d", false),
("a/**/c", "a/b/c", true),
("a/**/c", "a/b/d", false),
("a/**/c", "d/b/c", false),
// - * + * is covered above
// - * + **
("a*b**c", "abc", true),
("a*b**c", "aXbYc", true),
("a*b**c", "aXb/Yc", true),
("a*b**c", "aXbY/Yc", true),
("a*b**c", "aXb/Y/c", true),
("a*b**c", "a/XbYc", false),
("a*b**c", "aX/XbYc", false),
("a*b**c", "a/X/bYc", false),
// - ** + *
("a**b*c", "abc", true),
("a**b*c", "aXbYc", true),
("a**b*c", "aXb/Yc", false),
("a**b*c", "aXbY/Yc", false),
("a**b*c", "aXb/Y/c", false),
("a**b*c", "a/XbYc", true),
("a**b*c", "aX/XbYc", true),
("a**b*c", "a/X/bYc", true),
// - ** + **
("a**b**c", "abc", true),
("a**b**c", "aXbYc", true),
("a**b**c", "aXb/Yc", true),
("a**b**c", "aXbY/Yc", true),
("a**b**c", "aXb/Y/c", true),
("a**b**c", "aXbYc", true),
("a**b**c", "a/XbYc", true),
("a**b**c", "aX/XbYc", true),
("a**b**c", "a/X/bYc", true),
// Case insensitivity
("*.txt", "file.TXT", true),
("**/*.rs", "dir/file.RS", true),
// Optimized patterns: **/*.ext and **/name
("**/*.rs", "foo.rs", true),
("**/*.rs", "dir/foo.rs", true),
("**/*.rs", "dir/sub/foo.rs", true),
("**/*.rs", "foo.txt", false),
("**/*.rs", "dir/foo.txt", false),
("**/Cargo.toml", "Cargo.toml", true),
("**/Cargo.toml", "dir/Cargo.toml", true),
("**/Cargo.toml", "dir/sub/Cargo.toml", true),
("**/Cargo.toml", "Cargo.lock", false),
("**/Cargo.toml", "dir/Cargo.lock", false),
];
for (pattern, name, expected) in tests {
let result = glob_match(pattern, name);
assert_eq!(
result, expected,
"test case ({:?}, {:?}, {}) failed, got {}",
pattern, name, expected, result
);
}
}
}

View File

@ -3,6 +3,26 @@
//! Provides fast, non-cryptographic hash functions.
use std::hash::Hasher;
/// A [`Hasher`] implementation for the wyhash algorithm.
///
/// NOTE that you DO NOT want to use this for hashing mere strings/slices.
/// The stdlib [`Hash`] implementation for them calls [`Hasher::write`] twice,
/// once for the contents and once for a length prefix / `0xff` suffix.
#[derive(Default, Clone, Copy)]
pub struct WyHash(u64);
impl Hasher for WyHash {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, bytes: &[u8]) {
self.0 = hash(self.0, bytes);
}
}
/// The venerable wyhash hash function.
///
/// It's fast, has good statistical properties, and is in the public domain.

156
crates/edit/src/helpers.rs Normal file
View File

@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Random assortment of helpers I didn't know where to put.
use std::cmp::Ordering;
use std::io::{self, Read};
use std::mem::MaybeUninit;
use std::{fmt, slice};
pub const KILO: usize = 1000;
pub const MEGA: usize = 1000 * 1000;
pub const GIGA: usize = 1000 * 1000 * 1000;
pub const KIBI: usize = 1024;
pub const MEBI: usize = 1024 * 1024;
pub const GIBI: usize = 1024 * 1024 * 1024;
pub struct MetricFormatter<T>(pub T);
impl fmt::Display for MetricFormatter<usize> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut value = self.0;
let mut suffix = "B";
if value >= GIGA {
value /= GIGA;
suffix = "GB";
} else if value >= MEGA {
value /= MEGA;
suffix = "MB";
} else if value >= KILO {
value /= KILO;
suffix = "kB";
}
write!(f, "{value}{suffix}")
}
}
/// A viewport coordinate type used throughout the application.
pub type CoordType = isize;
/// To avoid overflow issues because you're adding two [`CoordType::MAX`]
/// values together, you can use [`COORD_TYPE_SAFE_MAX`] instead.
///
/// It equates to half the bits contained in [`CoordType`], which
/// for instance is 32767 (0x7FFF) when [`CoordType`] is a [`i32`].
pub const COORD_TYPE_SAFE_MAX: CoordType = (1 << (CoordType::BITS / 2 - 1)) - 1;
/// A 2D point. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Point {
pub x: CoordType,
pub y: CoordType,
}
impl Point {
pub const MIN: Self = Self { x: CoordType::MIN, y: CoordType::MIN };
pub const MAX: Self = Self { x: CoordType::MAX, y: CoordType::MAX };
}
impl PartialOrd<Self> for Point {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Point {
fn cmp(&self, other: &Self) -> Ordering {
self.y.cmp(&other.y).then(self.x.cmp(&other.x))
}
}
/// A 2D size. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Size {
pub width: CoordType,
pub height: CoordType,
}
impl Size {
pub fn as_rect(&self) -> Rect {
Rect { left: 0, top: 0, right: self.width, bottom: self.height }
}
}
/// A 2D rectangle. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub left: CoordType,
pub top: CoordType,
pub right: CoordType,
pub bottom: CoordType,
}
impl Rect {
/// Mimics CSS's `padding` property where `padding: a` is `a a a a`.
pub fn one(value: CoordType) -> Self {
Self { left: value, top: value, right: value, bottom: value }
}
/// Mimics CSS's `padding` property where `padding: a b` is `a b a b`,
/// and `a` is top/bottom and `b` is left/right.
pub fn two(top_bottom: CoordType, left_right: CoordType) -> Self {
Self { left: left_right, top: top_bottom, right: left_right, bottom: top_bottom }
}
/// Mimics CSS's `padding` property where `padding: a b c` is `a b c b`,
/// and `a` is top, `b` is left/right, and `c` is bottom.
pub fn three(top: CoordType, left_right: CoordType, bottom: CoordType) -> Self {
Self { left: left_right, top, right: left_right, bottom }
}
/// Is the rectangle empty?
pub fn is_empty(&self) -> bool {
self.left >= self.right || self.top >= self.bottom
}
/// Width of the rectangle.
pub fn width(&self) -> CoordType {
self.right - self.left
}
/// Height of the rectangle.
pub fn height(&self) -> CoordType {
self.bottom - self.top
}
/// Check if it contains a point.
pub fn contains(&self, point: Point) -> bool {
point.x >= self.left && point.x < self.right && point.y >= self.top && point.y < self.bottom
}
/// Intersect two rectangles.
pub fn intersect(&self, rhs: Self) -> Self {
let l = self.left.max(rhs.left);
let t = self.top.max(rhs.top);
let r = self.right.min(rhs.right);
let b = self.bottom.min(rhs.bottom);
// Ensure that the size is non-negative. This avoids bugs,
// because some height/width is negative all of a sudden.
let r = l.max(r);
let b = t.max(b);
Self { left: l, top: t, right: r, bottom: b }
}
}
/// [`Read`] but with [`MaybeUninit<u8>`] buffers.
pub fn file_read_uninit<T: Read>(file: &mut T, buf: &mut [MaybeUninit<u8>]) -> io::Result<usize> {
unsafe {
let buf_slice = slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len());
let n = file.read(buf_slice)?;
Ok(n)
}
}

View File

@ -4,16 +4,55 @@
//! Bindings to the ICU library.
use std::cmp::Ordering;
use std::ffi::CStr;
use std::mem;
use std::ffi::{CStr, c_char};
use std::mem::MaybeUninit;
use std::ops::Range;
use std::ptr::{null, null_mut};
use std::{fmt, mem};
use stdext::arena::{Arena, ArenaString, scratch_arena};
use stdext::arena_format;
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::buffer::TextBuffer;
use crate::sys;
use crate::unicode::Utf8Chars;
use crate::{apperr, arena_format, sys};
pub(crate) const ILLEGAL_ARGUMENT_ERROR: Error = Error(1); // U_ILLEGAL_ARGUMENT_ERROR
pub const ICU_MISSING_ERROR: Error = Error(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Error(u32);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fn format(code: u32) -> &'static str {
let Ok(f) = init_if_needed() else {
return "";
};
let status = icu_ffi::UErrorCode::new(code);
let ptr = unsafe { (f.u_errorName)(status) };
if ptr.is_null() {
return "";
}
let str = unsafe { CStr::from_ptr(ptr) };
str.to_str().unwrap_or("")
}
let code = self.0;
if code != 0
&& let msg = format(code)
&& !msg.is_empty()
{
write!(f, "ICU Error: {msg}")
} else {
write!(f, "ICU Error: {code:#08x}")
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Clone, Copy)]
pub struct Encoding {
@ -91,31 +130,6 @@ pub fn get_available_encodings() -> &'static Encodings {
}
}
/// Formats the given ICU error code into a human-readable string.
pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
fn format(code: u32) -> &'static str {
let Ok(f) = init_if_needed() else {
return "";
};
let status = icu_ffi::UErrorCode::new(code);
let ptr = unsafe { (f.u_errorName)(status) };
if ptr.is_null() {
return "";
}
let str = unsafe { CStr::from_ptr(ptr) };
str.to_str().unwrap_or("")
}
let msg = format(code);
if !msg.is_empty() {
write!(f, "ICU Error: {msg}")
} else {
write!(f, "ICU Error: {code:#08x}")
}
}
/// Converts between two encodings using ICU.
pub struct Converter<'pivot> {
source: *mut icu_ffi::UConverter,
@ -147,7 +161,7 @@ impl<'pivot> Converter<'pivot> {
pivot_buffer: &'pivot mut [MaybeUninit<u16>],
source_encoding: &str,
target_encoding: &str,
) -> apperr::Result<Self> {
) -> Result<Self> {
let f = init_if_needed()?;
let arena = scratch_arena(None);
@ -195,7 +209,7 @@ impl<'pivot> Converter<'pivot> {
&mut self,
input: &[u8],
output: &mut [MaybeUninit<u8>],
) -> apperr::Result<(usize, usize)> {
) -> Result<(usize, usize)> {
let f = assume_loaded();
let input_beg = input.as_ptr();
@ -301,7 +315,7 @@ impl Text {
///
/// The caller must ensure that the given [`TextBuffer`]
/// outlives the returned `Text` instance.
pub unsafe fn new(tb: &TextBuffer) -> apperr::Result<Self> {
pub unsafe fn new(tb: &TextBuffer) -> Result<Self> {
let f = init_if_needed()?;
let mut status = icu_ffi::U_ZERO_ERROR;
@ -520,11 +534,7 @@ fn utext_access_impl<'a>(
}
}
loop {
let Some(c) = it.next() else {
break;
};
while let Some(c) = it.next() {
// Thanks to our `if utf16_len >= UTF16_LEN_LIMIT` check,
// we can safely assume that this will fit.
unsafe {
@ -621,7 +631,7 @@ impl Regex {
/// # Safety
///
/// The caller must ensure that the given `Text` outlives the returned `Regex` instance.
pub unsafe fn new(pattern: &str, flags: i32, text: &Text) -> apperr::Result<Self> {
pub unsafe fn new(pattern: &str, flags: i32, text: &Text) -> Result<Self> {
let f = init_if_needed()?;
unsafe {
let scratch = scratch_arena(None);
@ -677,6 +687,31 @@ impl Regex {
let mut status = icu_ffi::U_ZERO_ERROR;
unsafe { (f.uregex_reset64)(self.0, offset as i64, &mut status) };
}
/// Gets captured group count.
pub fn group_count(&mut self) -> i32 {
let f = assume_loaded();
let mut status = icu_ffi::U_ZERO_ERROR;
let count = unsafe { (f.uregex_groupCount)(self.0, &mut status) };
if status.is_failure() { 0 } else { count }
}
/// Gets the text range of a captured group by index.
pub fn group(&mut self, group: i32) -> Option<Range<usize>> {
let f = assume_loaded();
let mut status = icu_ffi::U_ZERO_ERROR;
let start = unsafe { (f.uregex_start64)(self.0, group, &mut status) };
let end = unsafe { (f.uregex_end64)(self.0, group, &mut status) };
if status.is_failure() {
None
} else {
let start = start.max(0);
let end = end.max(start);
Some(start as usize..end as usize)
}
}
}
impl Iterator for Regex {
@ -691,15 +726,7 @@ impl Iterator for Regex {
return None;
}
let start = unsafe { (f.uregex_start64)(self.0, 0, &mut status) };
let end = unsafe { (f.uregex_end64)(self.0, 0, &mut status) };
if status.is_failure() {
return None;
}
let start = start.max(0);
let end = end.max(start);
Some(start as usize..end as usize)
self.group(0)
}
}
@ -900,36 +927,45 @@ struct LibraryFunctions {
uregex_setUText: icu_ffi::uregex_setUText,
uregex_reset64: icu_ffi::uregex_reset64,
uregex_findNext: icu_ffi::uregex_findNext,
uregex_groupCount: icu_ffi::uregex_groupCount,
uregex_start64: icu_ffi::uregex_start64,
uregex_end64: icu_ffi::uregex_end64,
}
macro_rules! proc_name {
($s:literal) => {
concat!(env!("EDIT_CFG_ICU_EXPORT_PREFIX"), $s, env!("EDIT_CFG_ICU_EXPORT_SUFFIX"), "\0")
.as_ptr() as *const c_char
};
}
// Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows.
const LIBICUUC_PROC_NAMES: [&CStr; 10] = [
c"u_errorName",
c"ucasemap_open",
c"ucasemap_utf8FoldCase",
c"ucnv_getAvailableName",
c"ucnv_getStandardName",
c"ucnv_open",
c"ucnv_close",
c"ucnv_convertEx",
c"utext_setup",
c"utext_close",
const LIBICUUC_PROC_NAMES: [*const c_char; 10] = [
proc_name!("u_errorName"),
proc_name!("ucasemap_open"),
proc_name!("ucasemap_utf8FoldCase"),
proc_name!("ucnv_getAvailableName"),
proc_name!("ucnv_getStandardName"),
proc_name!("ucnv_open"),
proc_name!("ucnv_close"),
proc_name!("ucnv_convertEx"),
proc_name!("utext_setup"),
proc_name!("utext_close"),
];
// Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows.
const LIBICUI18N_PROC_NAMES: [&CStr; 10] = [
c"ucol_open",
c"ucol_strcollUTF8",
c"uregex_open",
c"uregex_close",
c"uregex_setTimeLimit",
c"uregex_setUText",
c"uregex_reset64",
c"uregex_findNext",
c"uregex_start64",
c"uregex_end64",
const LIBICUI18N_PROC_NAMES: [*const c_char; 11] = [
proc_name!("ucol_open"),
proc_name!("ucol_strcollUTF8"),
proc_name!("uregex_open"),
proc_name!("uregex_close"),
proc_name!("uregex_setTimeLimit"),
proc_name!("uregex_setUText"),
proc_name!("uregex_reset64"),
proc_name!("uregex_findNext"),
proc_name!("uregex_groupCount"),
proc_name!("uregex_start64"),
proc_name!("uregex_end64"),
];
enum LibraryFunctionsState {
@ -940,22 +976,19 @@ enum LibraryFunctionsState {
static mut LIBRARY_FUNCTIONS: LibraryFunctionsState = LibraryFunctionsState::Uninitialized;
pub fn init() -> apperr::Result<()> {
pub fn init() -> Result<()> {
init_if_needed()?;
Ok(())
}
#[allow(static_mut_refs)]
fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
fn init_if_needed() -> Result<&'static LibraryFunctions> {
#[cold]
fn load() {
unsafe {
LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed;
let Ok(libicuuc) = sys::load_libicuuc() else {
return;
};
let Ok(libicui18n) = sys::load_libicui18n() else {
let Ok(icu) = sys::load_icu() else {
return;
};
@ -979,25 +1012,26 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
let mut funcs = MaybeUninit::<LibraryFunctions>::uninit();
let mut ptr = funcs.as_mut_ptr() as *mut TransparentFunction;
#[cfg(unix)]
#[cfg(edit_icu_renaming_auto_detect)]
let scratch_outer = scratch_arena(None);
#[cfg(unix)]
let suffix = sys::icu_proc_suffix(&scratch_outer, libicuuc);
#[cfg(edit_icu_renaming_auto_detect)]
let suffix = sys::icu_detect_renaming_suffix(&scratch_outer, icu.libicuuc);
for (handle, names) in
[(libicuuc, &LIBICUUC_PROC_NAMES[..]), (libicui18n, &LIBICUI18N_PROC_NAMES[..])]
{
for name in names {
#[cfg(unix)]
for (handle, names) in [
(icu.libicuuc, &LIBICUUC_PROC_NAMES[..]),
(icu.libicui18n, &LIBICUI18N_PROC_NAMES[..]),
] {
for &name in names {
#[cfg(edit_icu_renaming_auto_detect)]
let scratch = scratch_arena(Some(&scratch_outer));
#[cfg(unix)]
let name = &sys::add_icu_proc_suffix(&scratch, name, &suffix);
#[cfg(edit_icu_renaming_auto_detect)]
let name = sys::icu_add_renaming_suffix(&scratch, name, &suffix);
let Ok(func) = sys::get_proc_address(handle, name) else {
debug_assert!(
false,
"Failed to load ICU function: {}",
name.to_string_lossy()
"Failed to load ICU function: {:?}",
CStr::from_ptr(name)
);
return;
};
@ -1019,7 +1053,7 @@ fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
match unsafe { &LIBRARY_FUNCTIONS } {
LibraryFunctionsState::Loaded(f) => Ok(f),
_ => Err(apperr::APP_ICU_MISSING),
_ => Err(ICU_MISSING_ERROR),
}
}
@ -1036,7 +1070,7 @@ mod icu_ffi {
use std::ffi::{c_char, c_int, c_void};
use crate::apperr;
use super::Error;
#[derive(Copy, Clone, Eq, PartialEq)]
#[repr(transparent)]
@ -1055,9 +1089,9 @@ mod icu_ffi {
self.0 > 0
}
pub fn as_error(&self) -> apperr::Error {
pub fn as_error(&self) -> Error {
debug_assert!(self.0 > 0);
apperr::Error::new_icu(self.0 as u32)
Error(self.0 as u32)
}
}
@ -1277,6 +1311,8 @@ mod icu_ffi {
unsafe extern "C" fn(regexp: *mut URegularExpression, index: i64, status: &mut UErrorCode);
pub type uregex_findNext =
unsafe extern "C" fn(regexp: *mut URegularExpression, status: &mut UErrorCode) -> bool;
pub type uregex_groupCount =
unsafe extern "C" fn(regexp: *mut URegularExpression, status: &mut UErrorCode) -> i32;
pub type uregex_start64 = unsafe extern "C" fn(
regexp: *mut URegularExpression,
group_num: i32,
@ -1293,6 +1329,12 @@ mod icu_ffi {
mod tests {
use super::*;
#[ignore]
#[test]
fn init() {
assert!(init_if_needed().is_ok());
}
#[test]
fn test_compare_strings_ascii() {
// Empty strings

View File

@ -268,7 +268,7 @@ pub struct Parser {
bracketed_paste: bool,
bracketed_paste_buf: Vec<u8>,
x10_mouse_want: bool,
x10_mouse_buf: [u8; 3],
x10_mouse_buf: [char; 3],
x10_mouse_len: usize,
}
@ -281,7 +281,7 @@ impl Parser {
bracketed_paste: false,
bracketed_paste_buf: Vec::new(),
x10_mouse_want: false,
x10_mouse_buf: [0; 3],
x10_mouse_buf: ['\0'; 3],
x10_mouse_len: 0,
}
}
@ -461,7 +461,7 @@ impl<'input> Iterator for Stream<'_, '_, 'input> {
mouse.modifiers |=
if (btn & 0x08) != 0 { kbmod::ALT } else { kbmod::NONE };
mouse.modifiers |=
if (btn & 0x10f) != 0 { kbmod::CTRL } else { kbmod::NONE };
if (btn & 0x10) != 0 { kbmod::CTRL } else { kbmod::NONE };
mouse.position.x = csi.params[1] as CoordType - 1;
mouse.position.y = csi.params[2] as CoordType - 1;
@ -535,27 +535,35 @@ impl<'input> Stream<'_, '_, 'input> {
/// This is so puzzling to me. The existence of this function makes me unhappy.
#[cold]
fn parse_x10_mouse_coordinates(&mut self) -> Option<Input<'input>> {
self.parser.x10_mouse_len +=
self.stream.read(&mut self.parser.x10_mouse_buf[self.parser.x10_mouse_len..]);
while self.parser.x10_mouse_len < 3 && !self.stream.done() {
self.parser.x10_mouse_buf[self.parser.x10_mouse_len] = self.stream.next_char();
self.parser.x10_mouse_len += 1;
}
if self.parser.x10_mouse_len < 3 {
return None;
}
let button = self.parser.x10_mouse_buf[0] & 0b11;
let modifier = self.parser.x10_mouse_buf[0] & 0b11100;
let b = self.parser.x10_mouse_buf[0] as u32;
let x = self.parser.x10_mouse_buf[1] as CoordType - 0x21;
let y = self.parser.x10_mouse_buf[2] as CoordType - 0x21;
let action = match button {
let action = match b & 0b11 {
0 => InputMouseState::Left,
1 => InputMouseState::Middle,
2 => InputMouseState::Right,
_ => InputMouseState::None,
};
let modifiers = match modifier {
4 => kbmod::SHIFT,
8 => kbmod::ALT,
16 => kbmod::CTRL,
_ => kbmod::NONE,
let modifiers = {
let mut m = kbmod::NONE;
if (b & 0b00100) != 0 {
m |= kbmod::SHIFT;
}
if (b & 0b01000) != 0 {
m |= kbmod::ALT;
}
if (b & 0b10000) != 0 {
m |= kbmod::CTRL;
}
m
};
self.parser.x10_mouse_want = false;

645
crates/edit/src/json.rs Normal file
View File

@ -0,0 +1,645 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! A simple JSONC parser with trailing comma support.
//!
//! It's designed for parsing our small settings files,
//! but its performance is rather competitive in general.
use std::fmt;
use std::hint::unreachable_unchecked;
use stdext::arena::{Arena, ArenaString};
use crate::unicode::MeasurementConfig;
/// Maximum nesting depth to prevent stack overflow.
const MAX_DEPTH: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParseErrorKind {
/// Invalid JSON syntax
Syntax,
/// Maximum nesting depth exceeded
MaxDepth,
}
#[derive(Debug, Clone)]
pub struct ParseError {
kind: ParseErrorKind,
line: usize,
column: usize,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self.kind {
ParseErrorKind::Syntax => "Invalid JSON",
ParseErrorKind::MaxDepth => "JSON too deeply nested",
};
write!(f, "{}:{}: {}", self.line, self.column, message)
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone)]
pub enum Value<'a> {
Null,
Bool(bool),
Number(f64),
String(&'a str),
Array(&'a [Value<'a>]),
Object(&'a [(&'a str, Value<'a>)]),
}
impl<'a> Value<'a> {
pub fn is_null(&self) -> bool {
matches!(self, Value::Null)
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Value::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_number(&self) -> Option<f64> {
match self {
Value::Number(n) => Some(*n),
_ => None,
}
}
pub fn as_str(&self) -> Option<&'a str> {
match self {
Value::String(s) => Some(s),
_ => None,
}
}
pub fn as_array(&self) -> Option<&'a [Value<'a>]> {
match self {
Value::Array(arr) => Some(arr),
_ => None,
}
}
pub fn as_object(&self) -> Option<Object<'a>> {
match self {
Value::Object(entries) => Some(Object { entries }),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Object<'a> {
entries: &'a [(&'a str, Value<'a>)],
}
impl<'a> Object<'a> {
pub fn get(&self, key: &str) -> Option<&'a Value<'a>> {
self.entries.iter().find(|e| e.0 == key).map(|e| &e.1)
}
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.get(key).and_then(Value::as_bool)
}
pub fn get_number(&self, key: &str) -> Option<f64> {
self.get(key).and_then(Value::as_number)
}
pub fn get_str(&self, key: &str) -> Option<&'a str> {
self.get(key).and_then(Value::as_str)
}
pub fn get_array(&self, key: &str) -> Option<&'a [Value<'a>]> {
self.get(key).and_then(Value::as_array)
}
pub fn get_object(&self, key: &str) -> Option<Object<'a>> {
self.get(key).and_then(Value::as_object)
}
pub fn iter(&self) -> impl Iterator<Item = &'a (&'a str, Value<'a>)> {
self.entries.iter()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
pub fn parse<'a>(arena: &'a Arena, input: &str) -> Result<Value<'a>, ParseError> {
let mut parser = Parser::new(arena, input);
parser.skip_bom();
let value = parser.parse_value(0)?;
parser.skip_whitespace_and_comments()?;
if parser.pos == parser.input.len() {
Ok(value)
} else {
// Unexpected data after JSON value
Err(parser.fail(parser.pos, ParseErrorKind::Syntax))
}
}
struct Parser<'a, 'i> {
arena: &'a Arena,
input: &'i str,
bytes: &'i [u8],
pos: usize,
}
impl<'a, 'i> Parser<'a, 'i> {
fn new(arena: &'a Arena, input: &'i str) -> Self {
Self { arena, input, bytes: input.as_bytes(), pos: 0 }
}
fn parse_value(&mut self, depth: usize) -> Result<Value<'a>, ParseError> {
// Prevent stack overflow from deeply nested structures
if depth >= MAX_DEPTH {
return Err(self.fail(self.pos, ParseErrorKind::MaxDepth));
}
self.skip_whitespace_and_comments()?;
let ch = match self.peek() {
Some(ch) => ch,
// Unexpected end of input
None => return Err(self.fail(self.pos, ParseErrorKind::Syntax)),
};
match ch {
'n' => self.parse_null(),
't' => self.parse_true(),
'f' => self.parse_false(),
'-' | '0'..='9' => self.parse_number(),
'"' => self.parse_string(),
'[' => self.parse_array(depth),
'{' => self.parse_object(depth),
_ => Err(self.fail(self.pos, ParseErrorKind::Syntax)),
}
}
fn parse_null(&mut self) -> Result<Value<'a>, ParseError> {
self.expect_str("null")?;
Ok(Value::Null)
}
fn parse_true(&mut self) -> Result<Value<'a>, ParseError> {
self.expect_str("true")?;
Ok(Value::Bool(true))
}
fn parse_false(&mut self) -> Result<Value<'a>, ParseError> {
self.expect_str("false")?;
Ok(Value::Bool(false))
}
fn parse_number(&mut self) -> Result<Value<'a>, ParseError> {
let start = self.pos;
while self.pos < self.bytes.len()
&& matches!(self.bytes[self.pos], b'0'..=b'9' | b'.' | b'-' | b'+' | b'e' | b'E')
{
self.pos += 1;
}
if let Ok(num) = self.input[start..self.pos].parse::<f64>()
&& num.is_finite()
{
Ok(Value::Number(num))
} else {
Err(self.fail(self.pos, ParseErrorKind::Syntax))
}
}
fn parse_string(&mut self) -> Result<Value<'a>, ParseError> {
self.expect(b'"')?;
let mut result = ArenaString::new_in(self.arena);
loop {
if self.pos >= self.bytes.len() {
// Unterminated string
return Err(self.fail(self.pos, ParseErrorKind::Syntax));
}
let b = self.bytes[self.pos];
self.pos += 1;
match b {
b'"' => break,
b'\\' => self.parse_escape(&mut result)?,
..=0x1f => {
// Control characters must be escaped
return Err(self.fail(self.pos - 1, ParseErrorKind::Syntax));
}
_ => {
let beg = self.pos - 1;
while self.pos < self.bytes.len()
&& !matches!(self.bytes[self.pos], b'"' | b'\\' | ..=0x1f)
{
self.pos += 1;
}
result.push_str(&self.input[beg..self.pos]);
}
}
}
Ok(Value::String(result.leak()))
}
#[cold]
fn parse_escape(&mut self, result: &mut ArenaString) -> Result<(), ParseError> {
if self.pos >= self.bytes.len() {
// Unterminated escape sequence
return Err(self.fail(self.pos, ParseErrorKind::Syntax));
}
let b = self.bytes[self.pos];
self.pos += 1;
let ch = match b {
b'"' => b'"',
b'\\' => b'\\',
b'/' => b'/',
b'b' => b'\x08',
b'f' => b'\x0C',
b'n' => b'\n',
b'r' => b'\r',
b't' => b'\t',
b'u' => return self.parse_unicode_escape(result),
_ => {
// Invalid escape sequence
return Err(self.fail(self.pos - 2, ParseErrorKind::Syntax));
}
};
result.push(ch as char);
Ok(())
}
#[cold]
fn parse_unicode_escape(&mut self, result: &mut ArenaString) -> Result<(), ParseError> {
let start = self.pos - 2; // parse_escape() already advanced past "\u"
let mut code = self.parse_hex4()?;
if (0xd800..=0xdbff).contains(&code) {
if self.is_str("\\u")
&& let _ = self.advance(2)
&& let Ok(low) = self.parse_hex4()
&& (0xdc00..=0xdfff).contains(&low)
{
code = 0x10000 + ((code - 0xd800) << 10) + (low - 0xdc00);
} else {
code = u32::MAX;
};
}
match char::from_u32(code) {
Some(c) => {
result.push(c);
Ok(())
}
None => Err(self.fail(start, ParseErrorKind::Syntax)),
}
}
fn parse_hex4(&mut self) -> Result<u32, ParseError> {
let start = self.pos - 2; // parse_unicode_escape() already advanced past "\u"
self.bytes
.get(self.pos..self.pos + 4)
.and_then(|b| {
self.pos += 4;
b.iter().try_fold(0u32, |acc, &b| {
let d = (b as char).to_digit(16)?;
Some((acc << 4) | d)
})
})
.ok_or_else(|| self.fail(start, ParseErrorKind::Syntax))
}
fn parse_array(&mut self, depth: usize) -> Result<Value<'a>, ParseError> {
let mut values = Vec::new_in(self.arena);
let mut expects_comma = false;
self.expect(b'[')?;
loop {
self.skip_whitespace_and_comments()?;
match self.peek() {
// Unexpected end of input
None => return Err(self.fail(self.pos, ParseErrorKind::Syntax)),
Some(']') => break,
Some(',') => {
if !expects_comma {
// Unexpected comma
return Err(self.fail(self.pos, ParseErrorKind::Syntax));
}
self.advance(1);
self.skip_whitespace_and_comments()?;
expects_comma = false;
}
Some(_) => {
if expects_comma {
// Missing comma
return Err(self.fail(self.pos, ParseErrorKind::Syntax));
}
values.push(self.parse_value(depth + 1)?);
expects_comma = true;
}
}
}
self.expect(b']')?;
Ok(Value::Array(values.leak()))
}
fn parse_object(&mut self, depth: usize) -> Result<Value<'a>, ParseError> {
let mut entries = Vec::new_in(self.arena);
let mut expects_comma = false;
self.expect(b'{')?;
loop {
self.skip_whitespace_and_comments()?;
match self.peek() {
// Unexpected end of input
None => return Err(self.fail(self.pos, ParseErrorKind::Syntax)),
Some(',') => {
if !expects_comma {
// Unexpected comma
return Err(self.fail(self.pos, ParseErrorKind::Syntax));
}
self.advance(1);
self.skip_whitespace_and_comments()?;
expects_comma = false;
}
Some('}') => break,
Some(_) => {
if expects_comma {
// Missing comma
return Err(self.fail(self.pos, ParseErrorKind::Syntax));
}
let key = match self.parse_string()? {
Value::String(s) => s,
// The entire point of parse_string is to return a string.
// If that fails, we all should start farming potatoes.
// This is essentially an unwrap_unchecked().
_ => unsafe { unreachable_unchecked() },
};
self.skip_whitespace_and_comments()?;
self.expect(b':')?;
let value = self.parse_value(depth + 1)?;
entries.push((key, value));
expects_comma = true;
}
}
}
self.expect(b'}')?;
Ok(Value::Object(entries.leak()))
}
fn skip_bom(&mut self) {
if self.is_str("\u{feff}") {
self.advance(3);
}
}
fn skip_whitespace_and_comments(&mut self) -> Result<(), ParseError> {
loop {
loop {
if self.pos >= self.bytes.len() {
return Ok(());
}
match self.bytes[self.pos] {
b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1,
_ => break,
}
}
if self.is_str("//") {
self.pos += 2;
while self.pos < self.bytes.len() && self.bytes[self.pos] != b'\n' {
self.pos += 1;
}
} else if self.is_str("/*") {
let start = self.pos;
self.pos += 2;
loop {
while self.pos < self.bytes.len() && self.bytes[self.pos] != b'*' {
self.pos += 1;
}
if self.pos >= self.bytes.len() {
return Err(self.fail(start, ParseErrorKind::Syntax));
}
if self.is_str("*/") {
self.pos += 2;
break;
}
self.pos += 1;
}
} else {
return Ok(());
}
}
}
fn expect(&mut self, expected: u8) -> Result<(), ParseError> {
if self.bytes.get(self.pos) == Some(&expected) {
self.pos += 1;
Ok(())
} else {
Err(self.fail(self.pos, ParseErrorKind::Syntax))
}
}
fn expect_str(&mut self, expected: &str) -> Result<(), ParseError> {
if self.is_str(expected) {
self.pos += expected.len();
Ok(())
} else {
Err(self.fail(self.pos, ParseErrorKind::Syntax))
}
}
fn is_str(&self, expected: &str) -> bool {
self.bytes.get(self.pos..self.pos + expected.len()) == Some(expected.as_bytes())
}
fn peek(&self) -> Option<char> {
if self.pos < self.bytes.len() { Some(self.bytes[self.pos] as char) } else { None }
}
fn advance(&mut self, num: usize) {
self.pos += num;
}
#[cold]
fn fail(&self, pos: usize, kind: ParseErrorKind) -> ParseError {
let mut cfg = MeasurementConfig::new(&self.bytes);
let pos = cfg.goto_offset(pos);
let line = pos.logical_pos.y.max(0) as usize + 1;
let column = pos.logical_pos.x.max(0) as usize + 1;
ParseError { kind, line, column }
}
}
#[allow(non_snake_case)]
#[allow(clippy::invisible_characters)]
#[cfg(test)]
mod tests {
use stdext::arena::scratch_arena;
use super::*;
#[test]
fn test_null() {
let scratch = scratch_arena(None);
assert!(parse(&scratch, "null").unwrap().is_null());
}
#[test]
fn test_bool() {
let scratch = scratch_arena(None);
assert_eq!(parse(&scratch, "true").unwrap().as_bool(), Some(true));
assert_eq!(parse(&scratch, "false").unwrap().as_bool(), Some(false));
}
#[test]
fn test_number() {
let scratch = scratch_arena(None);
assert_eq!(parse(&scratch, "0").unwrap().as_number(), Some(0.0));
assert_eq!(parse(&scratch, "123").unwrap().as_number(), Some(123.0));
assert_eq!(parse(&scratch, "-456").unwrap().as_number(), Some(-456.0));
assert_eq!(parse(&scratch, "3.15").unwrap().as_number(), Some(3.15));
assert_eq!(parse(&scratch, "1e10").unwrap().as_number(), Some(1e10));
assert_eq!(parse(&scratch, "1.5e-3").unwrap().as_number(), Some(0.0015));
}
#[test]
fn test_string() {
let scratch = scratch_arena(None);
assert_eq!(parse(&scratch, r#""hello""#).unwrap().as_str(), Some("hello"));
assert_eq!(parse(&scratch, r#""hello\nworld""#).unwrap().as_str(), Some("hello\nworld"));
assert_eq!(parse(&scratch, r#""\u0041\u0042\u0043""#).unwrap().as_str(), Some("ABC"));
}
#[test]
fn test_array() {
let scratch = scratch_arena(None);
let value = parse(&scratch, "[1, 2, 3]").unwrap();
let arr = value.as_array().unwrap();
assert_eq!(arr.len(), 3);
assert_eq!(arr[0].as_number(), Some(1.0));
assert_eq!(arr[1].as_number(), Some(2.0));
assert_eq!(arr[2].as_number(), Some(3.0));
}
#[test]
fn test_object() {
let scratch = scratch_arena(None);
let value = parse(&scratch, r#"{"a": 1, "b": true}"#).unwrap();
let obj = value.as_object().unwrap();
assert_eq!(obj.get_number("a"), Some(1.0));
assert_eq!(obj.get_bool("b"), Some(true));
}
#[test]
fn test_comments() {
let scratch = scratch_arena(None);
let input = r#"{
// Line comment
"a": 1,
/* Block comment */
"b": 2
}"#;
let value = parse(&scratch, input).unwrap();
let obj = value.as_object().unwrap();
assert_eq!(obj.get_number("a"), Some(1.0));
assert_eq!(obj.get_number("b"), Some(2.0));
}
#[test]
fn test_trailing_comma() {
let scratch = scratch_arena(None);
assert!(parse(&scratch, "[1, 2, 3,]").is_ok());
assert!(parse(&scratch, r#"{"a": 1,}"#).is_ok());
}
#[test]
fn test_nested() {
let scratch = scratch_arena(None);
let input = r#"{
"nested": {
"array": [1, 2, {"deep": true}]
}
}"#;
let value = parse(&scratch, input).unwrap();
let obj = value.as_object().unwrap();
let nested = obj.get_object("nested").unwrap();
let array = nested.get_array("array").unwrap();
assert_eq!(array.len(), 3);
let deep_obj = array[2].as_object().unwrap();
assert_eq!(deep_obj.get_bool("deep"), Some(true));
}
#[test]
fn test_max_depth() {
let scratch = scratch_arena(None);
let mut input = String::new();
for _ in 0..100 {
input.push('[');
}
for _ in 0..100 {
input.push(']');
}
assert!(parse(&scratch, &input).is_err());
}
#[test]
fn test_invalid_json() {
let scratch = scratch_arena(None);
assert!(parse(&scratch, "").is_err());
assert!(parse(&scratch, "{").is_err());
assert!(parse(&scratch, r#"{"a":}"#).is_err());
assert!(parse(&scratch, r#"{5:1}"#).is_err());
assert!(parse(&scratch, "[1, 2,").is_err());
assert!(parse(&scratch, r#""unterminated"#).is_err());
}
#[test]
fn test_control_chars() {
let scratch = scratch_arena(None);
// Control characters must be escaped
assert!(parse(&scratch, "\"\x01\"").is_err());
}
#[test]
fn test_unicode() {
let scratch = scratch_arena(None);
// Test emoji (surrogate pair)
assert_eq!(parse(&scratch, r#""\uD83D\uDE00""#).unwrap().as_str(), Some("😀"));
// Test regular unicode
assert_eq!(parse(&scratch, r#""\u2764""#).unwrap().as_str(), Some(""));
}
}

View File

@ -5,18 +5,18 @@
allocator_api,
breakpoint,
cold_path,
let_chains,
linked_list_cursors,
maybe_uninit_fill,
maybe_uninit_slice,
maybe_uninit_uninit_array_transpose
)]
#![cfg_attr(
target_arch = "loongarch64",
feature(stdarch_loongarch, stdarch_loongarch_feature_detection, loongarch_target_feature),
allow(clippy::incompatible_msrv)
)]
#![allow(clippy::missing_transmute_annotations, clippy::new_without_default, stable_features)]
#[macro_use]
pub mod arena;
pub mod apperr;
pub mod base64;
pub mod buffer;
pub mod cell;
@ -24,10 +24,12 @@ pub mod clipboard;
pub mod document;
pub mod framebuffer;
pub mod fuzzy;
pub mod glob;
pub mod hash;
pub mod helpers;
pub mod icu;
pub mod input;
pub mod json;
pub mod oklab;
pub mod path;
pub mod simd;

View File

@ -7,76 +7,176 @@
#![allow(clippy::excessive_precision)]
/// An Oklab color with alpha.
pub struct Lab {
pub l: f32,
pub a: f32,
pub b: f32,
pub alpha: f32,
}
use std::fmt::Debug;
/// Converts a 32-bit sRGB color to Oklab.
pub fn srgb_to_oklab(color: u32) -> Lab {
let r = SRGB_TO_RGB_LUT[(color & 0xff) as usize];
let g = SRGB_TO_RGB_LUT[((color >> 8) & 0xff) as usize];
let b = SRGB_TO_RGB_LUT[((color >> 16) & 0xff) as usize];
let alpha = (color >> 24) as f32 * (1.0 / 255.0);
use crate::simd::MemsetSafe;
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
/// A sRGB color with straight (= not premultiplied) alpha.
#[derive(Default, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct StraightRgba(u32);
let l_ = cbrtf_est(l);
let m_ = cbrtf_est(m);
let s_ = cbrtf_est(s);
impl StraightRgba {
#[inline]
pub const fn zero() -> Self {
StraightRgba(0)
}
Lab {
l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
alpha,
#[inline]
pub const fn from_le(color: u32) -> Self {
StraightRgba(u32::from_le(color))
}
#[inline]
pub const fn from_be(color: u32) -> Self {
StraightRgba(u32::from_be(color))
}
#[inline]
pub const fn to_ne(self) -> u32 {
self.0
}
#[inline]
pub const fn to_le(self) -> u32 {
self.0.to_le()
}
#[inline]
pub const fn to_be(self) -> u32 {
self.0.to_be()
}
#[inline]
pub const fn red(self) -> u32 {
self.0 & 0xff
}
#[inline]
pub const fn green(self) -> u32 {
(self.0 >> 8) & 0xff
}
#[inline]
pub const fn blue(self) -> u32 {
(self.0 >> 16) & 0xff
}
#[inline]
pub const fn alpha(self) -> u32 {
self.0 >> 24
}
pub fn oklab_blend(self, top: StraightRgba) -> StraightRgba {
let bottom = self.as_oklab();
let top = top.as_oklab();
let result = bottom.blend(&top);
result.as_rgba()
}
pub fn as_oklab(self) -> Oklab {
let r = srgb_to_linear(self.red());
let g = srgb_to_linear(self.green());
let b = srgb_to_linear(self.blue());
let alpha = self.alpha() as f32 * (1.0 / 255.0);
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
let l_ = cbrtf_est(l);
let m_ = cbrtf_est(m);
let s_ = cbrtf_est(s);
let l = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
Oklab([l, a, b, alpha])
}
}
/// Converts an Oklab color to a 32-bit sRGB color.
pub fn oklab_to_srgb(c: Lab) -> u32 {
let l_ = c.l + 0.3963377774 * c.a + 0.2158037573 * c.b;
let m_ = c.l - 0.1055613458 * c.a - 0.0638541728 * c.b;
let s_ = c.l - 0.0894841775 * c.a - 1.2914855480 * c.b;
let l = l_ * l_ * l_;
let m = m_ * m_ * m_;
let s = s_ * s_ * s_;
let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
let r = r.clamp(0.0, 1.0);
let g = g.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
let alpha = c.alpha.clamp(0.0, 1.0);
let r = linear_to_srgb(r);
let g = linear_to_srgb(g);
let b = linear_to_srgb(b);
let a = (alpha * 255.0) as u32;
r | (g << 8) | (b << 16) | (a << 24)
impl Debug for StraightRgba {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "#{:08x}", self.0.to_be()) // Display as a hex color
}
}
/// Blends two 32-bit sRGB colors in the Oklab color space.
pub fn oklab_blend(dst: u32, src: u32) -> u32 {
let dst = srgb_to_oklab(dst);
let src = srgb_to_oklab(src);
unsafe impl MemsetSafe for StraightRgba {}
let inv_a = 1.0 - src.alpha;
let l = src.l + dst.l * inv_a;
let a = src.a + dst.a * inv_a;
let b = src.b + dst.b * inv_a;
let alpha = src.alpha + dst.alpha * inv_a;
/// An Oklab color with alpha. By convention, it uses straight alpha.
#[derive(Clone, Copy)]
pub struct Oklab([f32; 4]);
oklab_to_srgb(Lab { l, a, b, alpha })
impl Oklab {
#[inline]
pub const fn lightness(self) -> f32 {
self.0[0]
}
#[inline]
pub const fn a(self) -> f32 {
self.0[1]
}
#[inline]
pub const fn b(self) -> f32 {
self.0[2]
}
#[inline]
pub const fn alpha(self) -> f32 {
self.0[3]
}
pub fn as_rgba(&self) -> StraightRgba {
let l_ = self.lightness() + 0.3963377774 * self.a() + 0.2158037573 * self.b();
let m_ = self.lightness() - 0.1055613458 * self.a() - 0.0638541728 * self.b();
let s_ = self.lightness() - 0.0894841775 * self.a() - 1.2914855480 * self.b();
let l = l_ * l_ * l_;
let m = m_ * m_ * m_;
let s = s_ * s_ * s_;
let r = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
let g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
let b = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
let r = r.clamp(0.0, 1.0);
let g = g.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
let alpha = self.alpha().clamp(0.0, 1.0);
let r = linear_to_srgb(r);
let g = linear_to_srgb(g);
let b = linear_to_srgb(b);
let a = (alpha * 255.0) as u32;
StraightRgba(r | (g << 8) | (b << 16) | (a << 24))
}
/// Porter-Duff "over" composition. It's for Lab, but it works just like with RGB.
/// The benefit of the Oklab colorspace is its perceptual uniformity, which RGB lacks.
/// This can be observed easily when blending red and green for instance.
pub fn blend(&self, top: &Self) -> Self {
let top_a = top.alpha();
let bottom_a = self.alpha() * (1.0 - top_a);
let l = top.lightness() * top_a + self.lightness() * bottom_a;
let a = top.a() * top_a + self.a() * bottom_a;
let b = top.b() * top_a + self.b() * bottom_a;
let alpha = top_a + bottom_a;
let inv_alpha = if alpha > 0.0 { 1.0 / alpha } else { 0.0 };
let l = l * inv_alpha;
let a = a * inv_alpha;
let b = b * inv_alpha;
Self([l, a, b, alpha])
}
}
fn srgb_to_linear(c: u32) -> f32 {
SRGB_TO_RGB_LUT[(c & 0xff) as usize]
}
fn linear_to_srgb(c: f32) -> u32 {
@ -126,3 +226,17 @@ const SRGB_TO_RGB_LUT: [f32; 256] = [
0.7454043627, 0.7529423237, 0.7605246305, 0.7681512833, 0.7758223414, 0.7835379243, 0.7912980318, 0.7991028428, 0.8069523573, 0.8148466945, 0.8227858543, 0.8307699561, 0.8387991190, 0.8468732834, 0.8549926877, 0.8631572723,
0.8713672161, 0.8796223402, 0.8879231811, 0.8962693810, 0.9046613574, 0.9130986929, 0.9215820432, 0.9301108718, 0.9386858940, 0.9473065734, 0.9559735060, 0.9646862745, 0.9734454751, 0.9822505713, 0.9911022186, 1.0000000000,
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blending() {
let lower = StraightRgba::from_be(0x3498dbff);
let upper = StraightRgba::from_be(0xe74c3c7f);
let expected = StraightRgba::from_be(0xa67f93ff);
let blended = lower.oklab_blend(upper);
assert_eq!(blended, expected);
}
}

View File

@ -34,7 +34,7 @@ unsafe fn lines_bwd_raw(
line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
#[cfg(target_arch = "x86_64")]
#[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
return unsafe { LINES_BWD_DISPATCH(beg, end, line, line_stop) };
#[cfg(target_arch = "aarch64")]
@ -65,7 +65,7 @@ unsafe fn lines_bwd_fallback(
}
}
#[cfg(target_arch = "x86_64")]
#[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
static mut LINES_BWD_DISPATCH: unsafe fn(
beg: *const u8,
end: *const u8,
@ -109,10 +109,12 @@ unsafe fn lines_bwd_avx2(
}
let lf = _mm256_set1_epi8(b'\n' as i8);
let line_stop = line_stop.min(line);
let mut remaining = end.offset_from_unsigned(beg);
let off = end.addr() & 31;
if off != 0 && off < end.offset_from_unsigned(beg) {
(end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
}
while remaining >= 128 {
while end.offset_from_unsigned(beg) >= 128 {
let chunk_start = end.sub(128);
let v1 = _mm256_loadu_si256(chunk_start.add(0) as *const _);
@ -135,11 +137,10 @@ unsafe fn lines_bwd_avx2(
}
end = chunk_start;
remaining -= 128;
line = line_next;
}
while remaining >= 32 {
while end.offset_from_unsigned(beg) >= 32 {
let chunk_start = end.sub(32);
let v = _mm256_loadu_si256(chunk_start as *const _);
let c = _mm256_cmpeq_epi8(v, lf);
@ -154,7 +155,176 @@ unsafe fn lines_bwd_avx2(
}
end = chunk_start;
remaining -= 32;
line = line_next;
}
lines_bwd_fallback(beg, end, line, line_stop)
}
}
#[cfg(target_arch = "loongarch64")]
unsafe fn lines_bwd_dispatch(
beg: *const u8,
end: *const u8,
line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
use std::arch::is_loongarch_feature_detected;
let func = if is_loongarch_feature_detected!("lasx") {
lines_bwd_lasx
} else if is_loongarch_feature_detected!("lsx") {
lines_bwd_lsx
} else {
lines_bwd_fallback
};
unsafe { LINES_BWD_DISPATCH = func };
unsafe { func(beg, end, line, line_stop) }
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
unsafe fn lines_bwd_lasx(
beg: *const u8,
mut end: *const u8,
mut line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
unsafe {
use std::arch::loongarch64::*;
#[inline(always)]
unsafe fn horizontal_sum(sum: m256i) -> u32 {
unsafe {
let sum = lasx_xvhaddw_h_b(sum, sum);
let sum = lasx_xvhaddw_w_h(sum, sum);
let sum = lasx_xvhaddw_d_w(sum, sum);
let sum = lasx_xvhaddw_q_d(sum, sum);
let tmp = lasx_xvpermi_q::<1>(sum, sum);
let sum = lasx_xvadd_w(sum, tmp);
lasx_xvpickve2gr_wu::<0>(sum)
}
}
let lf = lasx_xvrepli_b(b'\n' as i32);
let line_stop = line_stop.min(line);
let off = end.addr() & 31;
if off != 0 && off < end.offset_from_unsigned(beg) {
(end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
}
while end.offset_from_unsigned(beg) >= 128 {
let chunk_start = end.sub(128);
let v1 = lasx_xvld::<0>(chunk_start as *const _);
let v2 = lasx_xvld::<32>(chunk_start as *const _);
let v3 = lasx_xvld::<64>(chunk_start as *const _);
let v4 = lasx_xvld::<96>(chunk_start as *const _);
let mut sum = lasx_xvrepli_b(0);
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v1, lf));
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v2, lf));
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v3, lf));
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v4, lf));
let sum = horizontal_sum(sum);
let line_next = line - sum as CoordType;
if line_next <= line_stop {
break;
}
end = chunk_start;
line = line_next;
}
while end.offset_from_unsigned(beg) >= 32 {
let chunk_start = end.sub(32);
let v = lasx_xvld::<0>(chunk_start as *const _);
let c = lasx_xvseq_b(v, lf);
let ones = lasx_xvand_v(c, lasx_xvrepli_b(1));
let sum = horizontal_sum(ones);
let line_next = line - sum as CoordType;
if line_next <= line_stop {
break;
}
end = chunk_start;
line = line_next;
}
lines_bwd_fallback(beg, end, line, line_stop)
}
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn lines_bwd_lsx(
beg: *const u8,
mut end: *const u8,
mut line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
unsafe {
use std::arch::loongarch64::*;
#[inline(always)]
unsafe fn horizontal_sum(sum: m128i) -> u32 {
unsafe {
let sum = lsx_vhaddw_h_b(sum, sum);
let sum = lsx_vhaddw_w_h(sum, sum);
let sum = lsx_vhaddw_d_w(sum, sum);
let sum = lsx_vhaddw_q_d(sum, sum);
lsx_vpickve2gr_wu::<0>(sum)
}
}
const LF: i32 = b'\n' as i32;
let line_stop = line_stop.min(line);
let off = end.addr() & 15;
if off != 0 && off < end.offset_from_unsigned(beg) {
(end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
}
while end.offset_from_unsigned(beg) >= 64 {
let chunk_start = end.sub(64);
let v1 = lsx_vld::<0>(chunk_start as *const _);
let v2 = lsx_vld::<16>(chunk_start as *const _);
let v3 = lsx_vld::<32>(chunk_start as *const _);
let v4 = lsx_vld::<48>(chunk_start as *const _);
let mut sum = lsx_vldi::<0>();
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v1));
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v2));
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v3));
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v4));
let sum = horizontal_sum(sum);
let line_next = line - sum as CoordType;
if line_next <= line_stop {
break;
}
end = chunk_start;
line = line_next;
}
while end.offset_from_unsigned(beg) >= 16 {
let chunk_start = end.sub(16);
let v = lsx_vld::<0>(chunk_start as *const _);
let c = lsx_vseqi_b::<LF>(v);
let ones = lsx_vandi_b::<1>(c);
let sum = horizontal_sum(ones);
let line_next = line - sum as CoordType;
if line_next <= line_stop {
break;
}
end = chunk_start;
line = line_next;
}
@ -174,9 +344,12 @@ unsafe fn lines_bwd_neon(
let lf = vdupq_n_u8(b'\n');
let line_stop = line_stop.min(line);
let mut remaining = end.offset_from_unsigned(beg);
let off = end.addr() & 15;
if off != 0 && off < end.offset_from_unsigned(beg) {
(end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
}
while remaining >= 64 {
while end.offset_from_unsigned(beg) >= 64 {
let chunk_start = end.sub(64);
let v1 = vld1q_u8(chunk_start.add(0));
@ -198,11 +371,10 @@ unsafe fn lines_bwd_neon(
}
end = chunk_start;
remaining -= 64;
line = line_next;
}
while remaining >= 16 {
while end.offset_from_unsigned(beg) >= 16 {
let chunk_start = end.sub(16);
let v = vld1q_u8(chunk_start);
let c = vceqq_u8(v, lf);
@ -215,7 +387,6 @@ unsafe fn lines_bwd_neon(
}
end = chunk_start;
remaining -= 16;
line = line_next;
}
@ -240,7 +411,7 @@ mod test {
for _ in 0..1000 {
let offset = offset_rng() % (text.len() + 1);
let line_stop = line_distance_rng() % (lines + 1);
let line = line_stop + line_rng() % 100;
let line = (line_stop + line_rng() % 100).saturating_sub(5);
let line = line as CoordType;
let line_stop = line_stop as CoordType;
@ -258,20 +429,19 @@ mod test {
mut line: CoordType,
line_stop: CoordType,
) -> (usize, CoordType) {
if line >= line_stop {
while offset > 0 {
let c = haystack[offset - 1];
if c == b'\n' {
if line == line_stop {
break;
}
line -= 1;
while offset > 0 {
let c = haystack[offset - 1];
if c == b'\n' {
if line <= line_stop {
break;
}
offset -= 1;
line -= 1;
}
offset -= 1;
}
(offset, line)
}
#[test]
fn seeks_to_start() {
for i in 6..=11 {

View File

@ -32,7 +32,7 @@ unsafe fn lines_fwd_raw(
line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
#[cfg(target_arch = "x86_64")]
#[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
return unsafe { LINES_FWD_DISPATCH(beg, end, line, line_stop) };
#[cfg(target_arch = "aarch64")]
@ -65,7 +65,7 @@ unsafe fn lines_fwd_fallback(
}
}
#[cfg(target_arch = "x86_64")]
#[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
static mut LINES_FWD_DISPATCH: unsafe fn(
beg: *const u8,
end: *const u8,
@ -109,12 +109,15 @@ unsafe fn lines_fwd_avx2(
}
let lf = _mm256_set1_epi8(b'\n' as i8);
let mut remaining = end.offset_from_unsigned(beg);
let off = beg.align_offset(32);
if off != 0 && off < end.offset_from_unsigned(beg) {
(beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
}
if line < line_stop {
// Unrolling the loop by 4x speeds things up by >3x.
// It allows us to accumulate matches before doing a single `vpsadbw`.
while remaining >= 128 {
while end.offset_from_unsigned(beg) >= 128 {
let v1 = _mm256_loadu_si256(beg.add(0) as *const _);
let v2 = _mm256_loadu_si256(beg.add(32) as *const _);
let v3 = _mm256_loadu_si256(beg.add(64) as *const _);
@ -138,11 +141,10 @@ unsafe fn lines_fwd_avx2(
}
beg = beg.add(128);
remaining -= 128;
line = line_next;
}
while remaining >= 32 {
while end.offset_from_unsigned(beg) >= 32 {
let v = _mm256_loadu_si256(beg as *const _);
let c = _mm256_cmpeq_epi8(v, lf);
@ -159,7 +161,172 @@ unsafe fn lines_fwd_avx2(
}
beg = beg.add(32);
remaining -= 32;
line = line_next;
}
}
lines_fwd_fallback(beg, end, line, line_stop)
}
}
#[cfg(target_arch = "loongarch64")]
unsafe fn lines_fwd_dispatch(
beg: *const u8,
end: *const u8,
line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
use std::arch::is_loongarch_feature_detected;
let func = if is_loongarch_feature_detected!("lasx") {
lines_fwd_lasx
} else if is_loongarch_feature_detected!("lsx") {
lines_fwd_lsx
} else {
lines_fwd_fallback
};
unsafe { LINES_FWD_DISPATCH = func };
unsafe { func(beg, end, line, line_stop) }
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
unsafe fn lines_fwd_lasx(
mut beg: *const u8,
end: *const u8,
mut line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
unsafe {
use std::arch::loongarch64::*;
#[inline(always)]
unsafe fn horizontal_sum(sum: m256i) -> u32 {
unsafe {
let sum = lasx_xvhaddw_h_b(sum, sum);
let sum = lasx_xvhaddw_w_h(sum, sum);
let sum = lasx_xvhaddw_d_w(sum, sum);
let sum = lasx_xvhaddw_q_d(sum, sum);
let tmp = lasx_xvpermi_q::<1>(sum, sum);
let sum = lasx_xvadd_w(sum, tmp);
lasx_xvpickve2gr_wu::<0>(sum)
}
}
let lf = lasx_xvrepli_b(b'\n' as i32);
let off = beg.align_offset(32);
if off != 0 && off < end.offset_from_unsigned(beg) {
(beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
}
if line < line_stop {
while end.offset_from_unsigned(beg) >= 128 {
let v1 = lasx_xvld::<0>(beg as *const _);
let v2 = lasx_xvld::<32>(beg as *const _);
let v3 = lasx_xvld::<64>(beg as *const _);
let v4 = lasx_xvld::<96>(beg as *const _);
let mut sum = lasx_xvrepli_b(0);
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v1, lf));
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v2, lf));
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v3, lf));
sum = lasx_xvsub_b(sum, lasx_xvseq_b(v4, lf));
let sum = horizontal_sum(sum);
let line_next = line + sum as CoordType;
if line_next >= line_stop {
break;
}
beg = beg.add(128);
line = line_next;
}
while end.offset_from_unsigned(beg) >= 32 {
let v = lasx_xvld::<0>(beg as *const _);
let c = lasx_xvseq_b(v, lf);
let ones = lasx_xvand_v(c, lasx_xvrepli_b(1));
let sum = horizontal_sum(ones);
let line_next = line + sum as CoordType;
if line_next >= line_stop {
break;
}
beg = beg.add(32);
line = line_next;
}
}
lines_fwd_fallback(beg, end, line, line_stop)
}
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn lines_fwd_lsx(
mut beg: *const u8,
end: *const u8,
mut line: CoordType,
line_stop: CoordType,
) -> (*const u8, CoordType) {
unsafe {
use std::arch::loongarch64::*;
#[inline(always)]
unsafe fn horizontal_sum(sum: m128i) -> u32 {
unsafe {
let sum = lsx_vhaddw_h_b(sum, sum);
let sum = lsx_vhaddw_w_h(sum, sum);
let sum = lsx_vhaddw_d_w(sum, sum);
let sum = lsx_vhaddw_q_d(sum, sum);
lsx_vpickve2gr_wu::<0>(sum)
}
}
const LF: i32 = b'\n' as i32;
let off = beg.align_offset(16);
if off != 0 && off < end.offset_from_unsigned(beg) {
(beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
}
if line < line_stop {
while end.offset_from_unsigned(beg) >= 64 {
let v1 = lsx_vld::<0>(beg as *const _);
let v2 = lsx_vld::<16>(beg as *const _);
let v3 = lsx_vld::<32>(beg as *const _);
let v4 = lsx_vld::<48>(beg as *const _);
let mut sum = lsx_vldi(0);
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v1));
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v2));
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v3));
sum = lsx_vsub_b(sum, lsx_vseqi_b::<LF>(v4));
let sum = horizontal_sum(sum);
let line_next = line + sum as CoordType;
if line_next >= line_stop {
break;
}
beg = beg.add(64);
line = line_next;
}
while end.offset_from_unsigned(beg) >= 16 {
let v = lsx_vld::<0>(beg as *const _);
let c = lsx_vseqi_b::<LF>(v);
let ones = lsx_vandi_b::<1>(c);
let sum = horizontal_sum(ones);
let line_next = line + sum as CoordType;
if line_next >= line_stop {
break;
}
beg = beg.add(16);
line = line_next;
}
}
@ -179,10 +346,13 @@ unsafe fn lines_fwd_neon(
use std::arch::aarch64::*;
let lf = vdupq_n_u8(b'\n');
let mut remaining = end.offset_from_unsigned(beg);
let off = beg.align_offset(16);
if off != 0 && off < end.offset_from_unsigned(beg) {
(beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
}
if line < line_stop {
while remaining >= 64 {
while end.offset_from_unsigned(beg) >= 64 {
let v1 = vld1q_u8(beg.add(0));
let v2 = vld1q_u8(beg.add(16));
let v3 = vld1q_u8(beg.add(32));
@ -204,11 +374,10 @@ unsafe fn lines_fwd_neon(
}
beg = beg.add(64);
remaining -= 64;
line = line_next;
}
while remaining >= 16 {
while end.offset_from_unsigned(beg) >= 16 {
let v = vld1q_u8(beg);
let c = vceqq_u8(v, lf);
let c = vandq_u8(c, vdupq_n_u8(0x01));
@ -220,7 +389,6 @@ unsafe fn lines_fwd_neon(
}
beg = beg.add(16);
remaining -= 16;
line = line_next;
}
}
@ -246,7 +414,7 @@ mod test {
for _ in 0..1000 {
let offset = offset_rng() % (text.len() + 1);
let line = line_rng() % 100;
let line_stop = line + line_distance_rng() % (lines + 1);
let line_stop = (line + line_distance_rng() % (lines + 1)).saturating_sub(5);
let line = line as CoordType;
let line_stop = line_stop as CoordType;

View File

@ -21,7 +21,7 @@ pub fn memchr2(needle1: u8, needle2: u8, haystack: &[u8], offset: usize) -> usiz
}
unsafe fn memchr2_raw(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
return unsafe { MEMCHR2_DISPATCH(needle1, needle2, beg, end) };
#[cfg(target_arch = "aarch64")]
@ -53,7 +53,7 @@ unsafe fn memchr2_fallback(
// itself to the correct implementation on the first call. This reduces binary size.
// It would also reduce branches if we had >2 implementations (a jump still needs to be predicted).
// NOTE that this ONLY works if Control Flow Guard is disabled on Windows.
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
static mut MEMCHR2_DISPATCH: unsafe fn(
needle1: u8,
needle2: u8,
@ -102,6 +102,89 @@ unsafe fn memchr2_avx2(needle1: u8, needle2: u8, mut beg: *const u8, end: *const
}
}
#[cfg(target_arch = "loongarch64")]
unsafe fn memchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
use std::arch::is_loongarch_feature_detected;
let func = if is_loongarch_feature_detected!("lasx") {
memchr2_lasx
} else if is_loongarch_feature_detected!("lsx") {
memchr2_lsx
} else {
memchr2_fallback
};
unsafe { MEMCHR2_DISPATCH = func };
unsafe { func(needle1, needle2, beg, end) }
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
unsafe fn memchr2_lasx(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
unsafe {
use std::arch::loongarch64::*;
let n1 = lasx_xvreplgr2vr_b(needle1 as i32);
let n2 = lasx_xvreplgr2vr_b(needle2 as i32);
let off = beg.align_offset(32);
if off != 0 && off < end.offset_from_unsigned(beg) {
beg = memchr2_lsx(needle1, needle2, beg, beg.add(off));
}
while end.offset_from_unsigned(beg) >= 32 {
let v = lasx_xvld::<0>(beg as *const _);
let a = lasx_xvseq_b(v, n1);
let b = lasx_xvseq_b(v, n2);
let c = lasx_xvor_v(a, b);
let m = lasx_xvmskltz_b(c);
let l = lasx_xvpickve2gr_wu::<0>(m);
let h = lasx_xvpickve2gr_wu::<4>(m);
let m = (h << 16) | l;
if m != 0 {
return beg.add(m.trailing_zeros() as usize);
}
beg = beg.add(32);
}
memchr2_fallback(needle1, needle2, beg, end)
}
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn memchr2_lsx(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
unsafe {
use std::arch::loongarch64::*;
let n1 = lsx_vreplgr2vr_b(needle1 as i32);
let n2 = lsx_vreplgr2vr_b(needle2 as i32);
let off = beg.align_offset(16);
if off != 0 && off < end.offset_from_unsigned(beg) {
beg = memchr2_fallback(needle1, needle2, beg, beg.add(off));
}
while end.offset_from_unsigned(beg) >= 16 {
let v = lsx_vld::<0>(beg as *const _);
let a = lsx_vseq_b(v, n1);
let b = lsx_vseq_b(v, n2);
let c = lsx_vor_v(a, b);
let m = lsx_vmskltz_b(c);
let m = lsx_vpickve2gr_wu::<0>(m);
if m != 0 {
return beg.add(m.trailing_zeros() as usize);
}
beg = beg.add(16);
}
memchr2_fallback(needle1, needle2, beg, end)
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn memchr2_neon(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
unsafe {
@ -142,8 +225,9 @@ unsafe fn memchr2_neon(needle1: u8, needle2: u8, mut beg: *const u8, end: *const
mod tests {
use std::slice;
use stdext::sys::{virtual_commit, virtual_reserve};
use super::*;
use crate::sys;
#[test]
fn test_empty() {
@ -182,8 +266,8 @@ mod tests {
const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures.
// 3 pages: uncommitted, committed, uncommitted
let ptr = sys::virtual_reserve(PAGE_SIZE * 3).unwrap();
sys::virtual_commit(ptr.add(PAGE_SIZE), PAGE_SIZE).unwrap();
let ptr = virtual_reserve(PAGE_SIZE * 3).unwrap();
virtual_commit(ptr.add(PAGE_SIZE), PAGE_SIZE).unwrap();
slice::from_raw_parts_mut(ptr.add(PAGE_SIZE).as_ptr(), PAGE_SIZE)
};

View File

@ -72,7 +72,7 @@ pub fn memset<T: MemsetSafe>(dst: &mut [T], val: T) {
#[inline]
fn memset_raw(beg: *mut u8, end: *mut u8, val: u64) {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
return unsafe { MEMSET_DISPATCH(beg, end, val) };
#[cfg(target_arch = "aarch64")]
@ -108,7 +108,7 @@ unsafe fn memset_fallback(mut beg: *mut u8, end: *mut u8, val: u64) {
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
static mut MEMSET_DISPATCH: unsafe fn(beg: *mut u8, end: *mut u8, val: u64) = memset_dispatch;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@ -235,6 +235,128 @@ fn memset_avx2(mut beg: *mut u8, end: *mut u8, val: u64) {
}
}
#[cfg(target_arch = "loongarch64")]
fn memset_dispatch(beg: *mut u8, end: *mut u8, val: u64) {
use std::arch::is_loongarch_feature_detected;
let func = if is_loongarch_feature_detected!("lasx") {
memset_lasx
} else if is_loongarch_feature_detected!("lsx") {
memset_lsx
} else {
memset_fallback
};
unsafe { MEMSET_DISPATCH = func };
unsafe { func(beg, end, val) }
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
fn memset_lasx(mut beg: *mut u8, end: *mut u8, val: u64) {
unsafe {
use std::arch::loongarch64::*;
let fill = lasx_xvreplgr2vr_d(val as i64);
if end.offset_from_unsigned(beg) >= 32 {
lasx_xvst::<0>(fill, beg as *mut _);
let off = beg.align_offset(32);
beg = beg.add(off);
}
if end.offset_from_unsigned(beg) >= 128 {
loop {
lasx_xvst::<0>(fill, beg as *mut _);
lasx_xvst::<32>(fill, beg as *mut _);
lasx_xvst::<64>(fill, beg as *mut _);
lasx_xvst::<96>(fill, beg as *mut _);
beg = beg.add(128);
if end.offset_from_unsigned(beg) < 128 {
break;
}
}
}
if end.offset_from_unsigned(beg) >= 16 {
let fill = lsx_vreplgr2vr_d(val as i64);
loop {
lsx_vst::<0>(fill, beg as *mut _);
beg = beg.add(16);
if end.offset_from_unsigned(beg) < 16 {
break;
}
}
}
if end.offset_from_unsigned(beg) >= 8 {
// 8-15 bytes
(beg as *mut u64).write_unaligned(val);
(end.sub(8) as *mut u64).write_unaligned(val);
} else if end.offset_from_unsigned(beg) >= 4 {
// 4-7 bytes
(beg as *mut u32).write_unaligned(val as u32);
(end.sub(4) as *mut u32).write_unaligned(val as u32);
} else if end.offset_from_unsigned(beg) >= 2 {
// 2-3 bytes
(beg as *mut u16).write_unaligned(val as u16);
(end.sub(2) as *mut u16).write_unaligned(val as u16);
} else if end.offset_from_unsigned(beg) >= 1 {
// 1 byte
beg.write(val as u8);
}
}
}
#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn memset_lsx(mut beg: *mut u8, end: *mut u8, val: u64) {
unsafe {
use std::arch::loongarch64::*;
if end.offset_from_unsigned(beg) >= 16 {
let fill = lsx_vreplgr2vr_d(val as i64);
lsx_vst::<0>(fill, beg as *mut _);
let off = beg.align_offset(16);
beg = beg.add(off);
while end.offset_from_unsigned(beg) >= 32 {
lsx_vst::<0>(fill, beg as *mut _);
lsx_vst::<16>(fill, beg as *mut _);
beg = beg.add(32);
}
if end.offset_from_unsigned(beg) >= 16 {
// 16-31 bytes remaining
lsx_vst::<0>(fill, beg as *mut _);
lsx_vst::<-16>(fill, end as *mut _);
return;
}
}
if end.offset_from_unsigned(beg) >= 8 {
// 8-15 bytes remaining
(beg as *mut u64).write_unaligned(val);
(end.sub(8) as *mut u64).write_unaligned(val);
} else if end.offset_from_unsigned(beg) >= 4 {
// 4-7 bytes remaining
(beg as *mut u32).write_unaligned(val as u32);
(end.sub(4) as *mut u32).write_unaligned(val as u32);
} else if end.offset_from_unsigned(beg) >= 2 {
// 2-3 bytes remaining
(beg as *mut u16).write_unaligned(val as u16);
(end.sub(2) as *mut u16).write_unaligned(val as u16);
} else if end.offset_from_unsigned(beg) >= 1 {
// 1 byte remaining
beg.write(val as u8);
}
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn memset_neon(mut beg: *mut u8, end: *mut u8, val: u64) {
unsafe {

View File

@ -6,32 +6,18 @@
//! Read the `windows` module for reference.
//! TODO: This reminds me that the sys API should probably be a trait.
use std::ffi::{CStr, c_int, c_void};
use std::fs::{self, File};
use std::ffi::{c_char, c_int, c_void};
use std::fs::File;
use std::mem::{self, ManuallyDrop, MaybeUninit};
use std::os::fd::{AsRawFd as _, FromRawFd as _};
use std::path::Path;
use std::ptr::{self, NonNull, null_mut};
use std::{thread, time};
use std::ptr::{NonNull, null_mut};
use std::{io, thread, time};
use stdext::arena::{Arena, ArenaString, scratch_arena};
use stdext::arena_format;
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;
use crate::{apperr, arena_format};
#[cfg(target_os = "netbsd")]
const fn desired_mprotect(flags: c_int) -> c_int {
// NetBSD allows an mmap(2) caller to specify what protection flags they
// will use later via mprotect. It does not allow a caller to move from
// PROT_NONE to PROT_READ | PROT_WRITE.
//
// see PROT_MPROTECT in man 2 mmap
flags << 3
}
#[cfg(not(target_os = "netbsd"))]
const fn desired_mprotect(_: c_int) -> c_int {
libc::PROT_NONE
}
struct State {
stdin: libc::c_int,
@ -60,7 +46,11 @@ extern "C" fn sigwinch_handler(_: libc::c_int) {
}
}
pub fn init() -> apperr::Result<Deinit> {
pub fn init() -> Deinit {
Deinit
}
pub fn switch_modes() -> io::Result<()> {
unsafe {
// Reopen stdin if it's redirected (= piped input).
if libc::isatty(STATE.stdin) == 0 {
@ -70,34 +60,14 @@ pub fn init() -> apperr::Result<Deinit> {
// Store the stdin flags so we can more easily toggle `O_NONBLOCK` later on.
STATE.stdin_flags = check_int_return(libc::fcntl(STATE.stdin, libc::F_GETFL))?;
Ok(Deinit)
}
}
pub struct Deinit;
impl Drop for Deinit {
fn drop(&mut self) {
unsafe {
#[allow(static_mut_refs)]
if let Some(termios) = STATE.stdout_initial_termios.take() {
// Restore the original terminal modes.
libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios);
}
}
}
}
pub fn switch_modes() -> apperr::Result<()> {
unsafe {
// Set STATE.inject_resize to true whenever we get a SIGWINCH.
let mut sigwinch_action: libc::sigaction = mem::zeroed();
sigwinch_action.sa_sigaction = sigwinch_handler as libc::sighandler_t;
sigwinch_action.sa_sigaction = sigwinch_handler as *const () as libc::sighandler_t;
check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?;
// Get the original terminal modes so we can disable raw mode on exit.
let mut termios = MaybeUninit::<libc::termios>::uninit();
check_int_return(libc::tcgetattr(STATE.stdin, termios.as_mut_ptr()))?;
check_int_return(libc::tcgetattr(STATE.stdout, termios.as_mut_ptr()))?;
let mut termios = termios.assume_init();
STATE.stdout_initial_termios = Some(termios);
@ -146,12 +116,26 @@ pub fn switch_modes() -> apperr::Result<()> {
// Set the terminal to raw mode.
termios.c_lflag &= !(libc::ICANON | libc::ECHO);
check_int_return(libc::tcsetattr(STATE.stdin, libc::TCSANOW, &termios))?;
check_int_return(libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios))?;
Ok(())
}
}
pub struct Deinit;
impl Drop for Deinit {
fn drop(&mut self) {
unsafe {
#[allow(static_mut_refs)]
if let Some(termios) = STATE.stdout_initial_termios.take() {
// Restore the original terminal modes.
libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios);
}
}
}
}
pub fn inject_window_size_into_stdin() {
unsafe {
STATE.inject_resize = true;
@ -202,8 +186,8 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaStr
// We got some leftover broken UTF8 from a previous read? Prepend it.
if STATE.utf8_len != 0 {
STATE.utf8_len = 0;
buf.extend_from_slice(&STATE.utf8_buf[..STATE.utf8_len]);
STATE.utf8_len = 0;
}
loop {
@ -218,7 +202,7 @@ pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaStr
tv_sec: timeout.as_secs() as libc::time_t,
tv_nsec: timeout.subsec_nanos() as libc::c_long,
};
ret = libc::ppoll(&mut pollfd, 1, &ts, ptr::null());
ret = libc::ppoll(&mut pollfd, 1, &ts, std::ptr::null());
}
#[cfg(not(target_os = "linux"))]
{
@ -367,7 +351,7 @@ pub struct FileId {
}
/// Returns a unique identifier for the given file by handle or path.
pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
pub fn file_id(file: Option<&File>, path: &Path) -> io::Result<FileId> {
let file = match file {
Some(f) => f,
None => &File::open(path)?,
@ -381,62 +365,10 @@ pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
}
}
/// Reserves a virtual memory region of the given size.
/// To commit the memory, use `virtual_commit`.
/// To release the memory, use `virtual_release`.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Don't forget to release the memory when you're done with it or you'll leak it.
pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
unsafe fn load_library(name: *const c_char) -> io::Result<NonNull<c_void>> {
unsafe {
let ptr = libc::mmap(
null_mut(),
size,
desired_mprotect(libc::PROT_READ | libc::PROT_WRITE),
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
);
if ptr.is_null() || ptr::eq(ptr, libc::MAP_FAILED) {
Err(errno_to_apperr(libc::ENOMEM))
} else {
Ok(NonNull::new_unchecked(ptr as *mut u8))
}
}
}
/// Releases a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from `virtual_reserve`.
pub unsafe fn virtual_release(base: NonNull<u8>, size: usize) {
unsafe {
libc::munmap(base.cast().as_ptr(), size);
}
}
/// Commits a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from `virtual_reserve`
/// and to pass a size less than or equal to the size passed to `virtual_reserve`.
pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<()> {
unsafe {
let status = libc::mprotect(base.cast().as_ptr(), size, libc::PROT_READ | libc::PROT_WRITE);
if status != 0 { Err(errno_to_apperr(libc::ENOMEM)) } else { Ok(()) }
}
}
unsafe fn load_library(name: &CStr) -> apperr::Result<NonNull<c_void>> {
unsafe {
NonNull::new(libc::dlopen(name.as_ptr(), libc::RTLD_LAZY))
.ok_or_else(|| errno_to_apperr(libc::ENOENT))
NonNull::new(libc::dlopen(name, libc::RTLD_LAZY))
.ok_or_else(|| from_raw_os_error(libc::ENOENT))
}
}
@ -448,31 +380,58 @@ unsafe fn load_library(name: &CStr) -> apperr::Result<NonNull<c_void>> {
/// of the function you're loading. No type checks whatsoever are performed.
//
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: &CStr) -> apperr::Result<T> {
pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: *const c_char) -> io::Result<T> {
unsafe {
let sym = libc::dlsym(handle.as_ptr(), name.as_ptr());
let sym = libc::dlsym(handle.as_ptr(), name);
if sym.is_null() {
Err(errno_to_apperr(libc::ENOENT))
Err(from_raw_os_error(libc::ENOENT))
} else {
Ok(mem::transmute_copy(&sym))
}
}
}
pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(c"libicuuc.so") }
pub struct LibIcu {
pub libicuuc: NonNull<c_void>,
pub libicui18n: NonNull<c_void>,
}
pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(c"libicui18n.so") }
pub fn load_icu() -> io::Result<LibIcu> {
const fn const_str_eq(a: &str, b: &str) -> bool {
let a = a.as_bytes();
let b = b.as_bytes();
let mut i = 0;
loop {
if i >= a.len() || i >= b.len() {
return a.len() == b.len();
}
if a[i] != b[i] {
return false;
}
i += 1;
}
}
const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0");
const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0");
if const { const_str_eq(LIBICUUC, LIBICUI18N) } {
let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? };
Ok(LibIcu { libicuuc: icu, libicui18n: icu })
} else {
let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? };
let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? };
Ok(LibIcu { libicuuc, libicui18n })
}
}
/// ICU, by default, adds the major version as a suffix to each exported symbol.
/// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`),
/// but I found that many (most?) Linux distributions don't do this for some reason.
/// This function returns the suffix, if any.
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_> {
#[cfg(edit_icu_renaming_auto_detect)]
pub fn icu_detect_renaming_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_> {
unsafe {
type T = *const c_void;
@ -480,7 +439,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
// Check if the ICU library is using unversioned symbols.
// Return an empty suffix in that case.
if get_proc_address::<T>(handle, c"u_errorName").is_ok() {
if get_proc_address::<T>(handle, c"u_errorName".as_ptr()).is_ok() {
return res;
}
@ -488,7 +447,7 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
// this symbol seems to be always present. This allows us to call `dladdr`.
// It's the `UCaseMap::~UCaseMap()` destructor which for some reason isn't
// in a namespace. Thank you ICU maintainers for this oversight.
let proc = match get_proc_address::<T>(handle, c"_ZN8UCaseMapD1Ev") {
let proc = match get_proc_address::<T>(handle, c"_ZN8UCaseMapD1Ev".as_ptr()) {
Ok(proc) => proc,
Err(_) => return res,
};
@ -501,12 +460,12 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
}
// The library path is in `info.dli_fname`.
let path = match CStr::from_ptr(info.dli_fname).to_str() {
let path = match std::ffi::CStr::from_ptr(info.dli_fname).to_str() {
Ok(name) => name,
Err(_) => return res,
};
let path = match fs::read_link(path) {
let path = match std::fs::read_link(path) {
Ok(path) => path,
Err(_) => path.into(),
};
@ -528,7 +487,13 @@ pub fn icu_proc_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_
}
}
pub fn add_icu_proc_suffix<'a, 'b, 'r>(arena: &'a Arena, name: &'b CStr, suffix: &str) -> &'r CStr
#[cfg(edit_icu_renaming_auto_detect)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn icu_add_renaming_suffix<'a, 'b, 'r>(
arena: &'a Arena,
name: *const c_char,
suffix: &str,
) -> *const c_char
where
'a: 'r,
'b: 'r,
@ -538,16 +503,15 @@ where
} else {
// SAFETY: In this particular case we know that the string
// is valid UTF-8, because it comes from icu.rs.
let name = unsafe { std::ffi::CStr::from_ptr(name) };
let name = unsafe { name.to_str().unwrap_unchecked() };
let mut res = ArenaString::new_in(arena);
let mut res = ManuallyDrop::new(ArenaString::new_in(arena));
res.reserve(name.len() + suffix.len() + 1);
res.push_str(name);
res.push_str(suffix);
res.push('\0');
let bytes: &'a [u8] = unsafe { mem::transmute(res.as_bytes()) };
unsafe { CStr::from_bytes_with_nul_unchecked(bytes) }
res.as_ptr() as *const c_char
}
}
@ -573,40 +537,29 @@ pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString<'_>, &Arena> {
}
#[inline]
fn errno() -> i32 {
#[cold]
fn errno() -> c_int {
// libc unfortunately doesn't export an alias for `errno` (WHY?).
// As such we (ab)use the stdlib and use its internal errno implementation.
//
// Under `-O -Copt-level=s` the 1.87 compiler fails to fully inline and
// remove the raw_os_error() call. This leaves us with the drop() call.
// ManuallyDrop fixes that and results in a direct `std::sys::os::errno` call.
ManuallyDrop::new(std::io::Error::last_os_error()).raw_os_error().unwrap_or(0)
ManuallyDrop::new(io::Error::last_os_error()).raw_os_error().unwrap_or(0)
}
#[inline]
pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
errno_to_apperr(err.raw_os_error().unwrap_or(0))
#[cold]
fn last_os_error() -> io::Error {
io::Error::last_os_error()
}
pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
write!(f, "Error {code}")?;
unsafe {
let ptr = libc::strerror(code as i32);
if !ptr.is_null() {
let msg = CStr::from_ptr(ptr).to_string_lossy();
write!(f, ": {msg}")?;
}
}
Ok(())
#[inline]
#[cold]
fn from_raw_os_error(code: c_int) -> io::Error {
io::Error::from_raw_os_error(code)
}
pub fn apperr_is_not_found(err: apperr::Error) -> bool {
err == errno_to_apperr(libc::ENOENT)
}
const fn errno_to_apperr(no: c_int) -> apperr::Error {
apperr::Error::new_sys(if no < 0 { 0 } else { no as u32 })
}
fn check_int_return(ret: libc::c_int) -> apperr::Result<libc::c_int> {
if ret < 0 { Err(errno_to_apperr(errno())) } else { Ok(ret) }
fn check_int_return(ret: libc::c_int) -> io::Result<libc::c_int> {
if ret < 0 { Err(last_os_error()) } else { Ok(ret) }
}

View File

@ -1,32 +1,59 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::{CStr, OsString, c_void};
use std::ffi::{OsString, c_char, c_void};
use std::fmt::Write as _;
use std::fs::{self, File};
use std::mem::MaybeUninit;
use std::os::windows::io::{AsRawHandle as _, FromRawHandle};
use std::path::{Path, PathBuf};
use std::ptr::{self, NonNull, null, null_mut};
use std::{mem, time};
use std::{io, mem, time};
use stdext::arena::{Arena, ArenaString, scratch_arena};
use windows_sys::Win32::Storage::FileSystem;
use windows_sys::Win32::System::Diagnostics::Debug;
use windows_sys::Win32::System::{Console, IO, LibraryLoader, Memory, Threading};
use windows_sys::Win32::System::{Console, IO, LibraryLoader, Threading};
use windows_sys::Win32::{Foundation, Globalization};
use windows_sys::w;
use windows_sys::core::*;
use crate::apperr;
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;
macro_rules! w_env {
($s:literal) => {{
const INPUT: &[u8] = env!($s).as_bytes();
const OUTPUT_LEN: usize = windows_sys::core::utf16_len(INPUT) + 1;
const OUTPUT: &[u16; OUTPUT_LEN] = {
let mut buffer = [0; OUTPUT_LEN];
let mut input_pos = 0;
let mut output_pos = 0;
while let Some((mut code_point, new_pos)) =
windows_sys::core::decode_utf8_char(INPUT, input_pos)
{
input_pos = new_pos;
if code_point <= 0xffff {
buffer[output_pos] = code_point as u16;
output_pos += 1;
} else {
code_point -= 0x10000;
buffer[output_pos] = 0xd800 + (code_point >> 10) as u16;
output_pos += 1;
buffer[output_pos] = 0xdc00 + (code_point & 0x3ff) as u16;
output_pos += 1;
}
}
&{ buffer }
};
OUTPUT.as_ptr()
}};
}
type ReadConsoleInputExW = unsafe extern "system" fn(
h_console_input: Foundation::HANDLE,
lp_buffer: *mut Console::INPUT_RECORD,
n_length: u32,
lp_number_of_events_read: *mut u32,
w_flags: u16,
) -> Foundation::BOOL;
) -> BOOL;
unsafe extern "system" fn read_console_input_ex_placeholder(
_: Foundation::HANDLE,
@ -34,12 +61,11 @@ unsafe extern "system" fn read_console_input_ex_placeholder(
_: u32,
_: *mut u32,
_: u16,
) -> Foundation::BOOL {
) -> BOOL {
panic!();
}
const CONSOLE_READ_NOWAIT: u16 = 0x0002;
const INVALID_CONSOLE_MODE: u32 = u32::MAX;
struct State {
@ -68,7 +94,7 @@ static mut STATE: State = State {
wants_exit: false,
};
extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL {
extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> BOOL {
unsafe {
STATE.wants_exit = true;
IO::CancelIoEx(STATE.stdin, null());
@ -77,19 +103,43 @@ extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL {
}
/// Initializes the platform-specific state.
pub fn init() -> apperr::Result<Deinit> {
pub fn init() -> Deinit {
unsafe {
// Get the stdin and stdout handles first, so that if this function fails,
// we at least got something to use for `write_stdout`.
STATE.stdin = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
STATE.stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE);
Deinit
}
}
/// Switches the terminal into raw mode, etc.
pub fn switch_modes() -> io::Result<()> {
unsafe {
// `kernel32.dll` doesn't exist on OneCore variants of Windows.
// NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though.
//
// This is written as two nested `match` statements so that we can return the error from the first
// `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information,
// while the kernelbase.dll lookup may not, since it's not a stable API.
unsafe fn load_read_func(module: *const u16) -> io::Result<ReadConsoleInputExW> {
unsafe {
get_module(module)
.and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr()))
}
}
STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) {
Ok(func) => func,
Err(err) => match load_read_func(w!("kernelbase.dll")) {
Ok(func) => func,
Err(_) => return Err(err),
},
};
// Reopen stdin if it's redirected (= piped input).
if !ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
&& matches!(
FileSystem::GetFileType(STATE.stdin),
FileSystem::FILE_TYPE_DISK | FileSystem::FILE_TYPE_PIPE
)
if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
|| !matches!(FileSystem::GetFileType(STATE.stdin), FileSystem::FILE_TYPE_CHAR)
{
STATE.stdin = FileSystem::CreateFileW(
w!("CONIN$"),
@ -101,32 +151,43 @@ pub fn init() -> apperr::Result<Deinit> {
null_mut(),
);
}
if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
|| ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE)
{
return Err(get_last_error());
return Err(last_os_error());
}
unsafe fn load_read_func(module: *const u16) -> apperr::Result<ReadConsoleInputExW> {
unsafe { get_module(module).and_then(|m| get_proc_address(m, c"ReadConsoleInputExW")) }
}
check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?;
check_bool_return(Console::GetConsoleMode(STATE.stdout, &raw mut STATE.stdout_mode_old))?;
// `kernel32.dll` doesn't exist on OneCore variants of Windows.
// NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though.
//
// This is written as two nested `match` statements so that we can return the error from the first
// `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information,
// while the kernelbase.dll lookup may not, since it's not a stable API.
STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) {
Ok(func) => func,
Err(err) => match load_read_func(w!("kernelbase.dll")) {
Ok(func) => func,
Err(_) => return Err(err),
},
};
match check_bool_return(Console::SetConsoleMode(
STATE.stdin,
Console::ENABLE_WINDOW_INPUT
| Console::ENABLE_EXTENDED_FLAGS
| Console::ENABLE_VIRTUAL_TERMINAL_INPUT,
)) {
Err(e) if e.kind() == io::ErrorKind::InvalidInput => {
Err(io::Error::other("This application does not support the legacy console."))
}
other => other,
}?;
check_bool_return(Console::SetConsoleMode(
STATE.stdout,
Console::ENABLE_PROCESSED_OUTPUT
| Console::ENABLE_WRAP_AT_EOL_OUTPUT
| Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING
| Console::DISABLE_NEWLINE_AUTO_RETURN,
))?;
Ok(Deinit)
check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?;
STATE.stdin_cp_old = Console::GetConsoleCP();
STATE.stdout_cp_old = Console::GetConsoleOutputCP();
check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?;
check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?;
Ok(())
}
}
@ -155,36 +216,6 @@ impl Drop for Deinit {
}
}
/// Switches the terminal into raw mode, etc.
pub fn switch_modes() -> apperr::Result<()> {
unsafe {
check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?;
STATE.stdin_cp_old = Console::GetConsoleCP();
STATE.stdout_cp_old = Console::GetConsoleOutputCP();
check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?;
check_bool_return(Console::GetConsoleMode(STATE.stdout, &raw mut STATE.stdout_mode_old))?;
check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?;
check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?;
check_bool_return(Console::SetConsoleMode(
STATE.stdin,
Console::ENABLE_WINDOW_INPUT
| Console::ENABLE_EXTENDED_FLAGS
| Console::ENABLE_VIRTUAL_TERMINAL_INPUT,
))?;
check_bool_return(Console::SetConsoleMode(
STATE.stdout,
Console::ENABLE_PROCESSED_OUTPUT
| Console::ENABLE_WRAP_AT_EOL_OUTPUT
| Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING
| Console::DISABLE_NEWLINE_AUTO_RETURN,
))?;
Ok(())
}
}
/// During startup we need to get the window size from the terminal.
/// Because I didn't want to type a bunch of code, this function tells
/// [`read_stdin`] to inject a fake sequence, which gets picked up by
@ -437,7 +468,7 @@ impl PartialEq for FileId {
impl Eq for FileId {}
/// Returns a unique identifier for the given file by handle or path.
pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
pub fn file_id(file: Option<&File>, path: &Path) -> io::Result<FileId> {
let file = match file {
Some(f) => f,
None => &File::open(path)?,
@ -446,7 +477,7 @@ pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
file_id_from_handle(file).or_else(|_| Ok(FileId::Path(std::fs::canonicalize(path)?)))
}
fn file_id_from_handle(file: &File) -> apperr::Result<FileId> {
fn file_id_from_handle(file: &File) -> io::Result<FileId> {
unsafe {
let mut info = MaybeUninit::<FileSystem::FILE_ID_INFO>::uninit();
check_bool_return(FileSystem::GetFileInformationByHandleEx(
@ -478,75 +509,11 @@ pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
Ok(path)
}
/// Reserves a virtual memory region of the given size.
/// To commit the memory, use [`virtual_commit`].
/// To release the memory, use [`virtual_release`].
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Don't forget to release the memory when you're done with it or you'll leak it.
pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
unsafe {
#[allow(unused_assignments, unused_mut)]
let mut base = null_mut();
// In debug builds, we use fixed addresses to aid in debugging.
// Makes it possible to immediately tell which address space a pointer belongs to.
#[cfg(all(debug_assertions, not(target_pointer_width = "32")))]
{
static mut S_BASE_GEN: usize = 0x0000100000000000; // 16 TiB
S_BASE_GEN += 0x0000001000000000; // 64 GiB
base = S_BASE_GEN as *mut _;
}
check_ptr_return(Memory::VirtualAlloc(
base,
size,
Memory::MEM_RESERVE,
Memory::PAGE_READWRITE,
) as *mut u8)
}
}
/// Releases a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`].
pub unsafe fn virtual_release(base: NonNull<u8>, _size: usize) {
unsafe {
// NOTE: `VirtualFree` fails if the pointer isn't
// a valid base address or if the size isn't zero.
Memory::VirtualFree(base.as_ptr() as *mut _, 0, Memory::MEM_RELEASE);
}
}
/// Commits a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`]
/// and to pass a size less than or equal to the size passed to [`virtual_reserve`].
pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<()> {
unsafe {
check_ptr_return(Memory::VirtualAlloc(
base.as_ptr() as *mut _,
size,
Memory::MEM_COMMIT,
Memory::PAGE_READWRITE,
))
.map(|_| ())
}
}
unsafe fn get_module(name: *const u16) -> apperr::Result<NonNull<c_void>> {
unsafe fn get_module(name: *const u16) -> io::Result<NonNull<c_void>> {
unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) }
}
unsafe fn load_library(name: *const u16) -> apperr::Result<NonNull<c_void>> {
unsafe fn load_library(name: *const u16) -> io::Result<NonNull<c_void>> {
unsafe {
check_ptr_return(LibraryLoader::LoadLibraryExW(
name,
@ -564,21 +531,47 @@ unsafe fn load_library(name: *const u16) -> apperr::Result<NonNull<c_void>> {
/// of the function you're loading. No type checks whatsoever are performed.
//
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: &CStr) -> apperr::Result<T> {
pub unsafe fn get_proc_address<T>(handle: NonNull<c_void>, name: *const c_char) -> io::Result<T> {
unsafe {
let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name.as_ptr() as *const u8);
if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) }
let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8);
if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(last_os_error()) }
}
}
/// Loads the "common" portion of ICU4C.
pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(w!("icuuc.dll")) }
pub struct LibIcu {
pub libicuuc: NonNull<c_void>,
pub libicui18n: NonNull<c_void>,
}
/// Loads the internationalization portion of ICU4C.
pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(w!("icuin.dll")) }
pub fn load_icu() -> io::Result<LibIcu> {
const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool {
unsafe {
let mut a = a;
let mut b = b;
loop {
if *a != *b {
return false;
}
if *a == 0 {
return true;
}
a = a.add(1);
b = b.add(1);
}
}
}
const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME");
const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME");
if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } {
let icu = unsafe { load_library(LIBICUUC)? };
Ok(LibIcu { libicuuc: icu, libicui18n: icu })
} else {
let libicuuc = unsafe { load_library(LIBICUUC)? };
let libicui18n = unsafe { load_library(LIBICUI18N)? };
Ok(LibIcu { libicuuc, libicui18n })
}
}
/// Returns a list of preferred languages for the current user.
@ -651,60 +644,16 @@ fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> {
res
}
#[inline]
#[cold]
fn get_last_error() -> apperr::Error {
unsafe { gle_to_apperr(Foundation::GetLastError()) }
fn last_os_error() -> io::Error {
io::Error::last_os_error()
}
#[inline]
const fn gle_to_apperr(gle: u32) -> apperr::Error {
apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle })
fn check_bool_return(ret: BOOL) -> io::Result<()> {
if ret == 0 { Err(last_os_error()) } else { Ok(()) }
}
#[inline]
pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32)
}
/// Formats a platform error code into a human-readable string.
pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
unsafe {
let mut ptr: *mut u8 = null_mut();
let len = Debug::FormatMessageA(
Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER
| Debug::FORMAT_MESSAGE_FROM_SYSTEM
| Debug::FORMAT_MESSAGE_IGNORE_INSERTS,
null(),
code,
0,
&mut ptr as *mut *mut _ as *mut _,
0,
null_mut(),
);
write!(f, "Error {code:#08x}")?;
if len > 0 {
let msg = str_from_raw_parts(ptr, len as usize);
let msg = msg.trim_ascii();
let msg = msg.replace(['\r', '\n'], " ");
write!(f, ": {msg}")?;
Foundation::LocalFree(ptr as *mut _);
}
Ok(())
}
}
/// Checks if the given error is a "file not found" error.
pub fn apperr_is_not_found(err: apperr::Error) -> bool {
err == gle_to_apperr(Foundation::ERROR_FILE_NOT_FOUND)
}
fn check_bool_return(ret: Foundation::BOOL) -> apperr::Result<()> {
if ret == 0 { Err(get_last_error()) } else { Ok(()) }
}
fn check_ptr_return<T>(ret: *mut T) -> apperr::Result<NonNull<T>> {
NonNull::new(ret).ok_or_else(get_last_error)
fn check_ptr_return<T>(ret: *mut T) -> io::Result<NonNull<T>> {
NonNull::new(ret).ok_or_else(last_os_error)
}

View File

@ -92,7 +92,7 @@
//! use edit::helpers::Size;
//! use edit::input::Input;
//! use edit::tui::*;
//! use edit::{arena, arena_format};
//! use stdext::{arena, arena_format};
//!
//! struct State {
//! counter: i32,
@ -147,10 +147,12 @@ use std::arch::breakpoint;
#[cfg(debug_assertions)]
use std::collections::HashSet;
use std::fmt::Write as _;
use std::{iter, mem, ptr, time};
use std::{io, iter, mem, ptr, time};
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell};
use stdext::arena::{Arena, ArenaString, scratch_arena};
use stdext::{arena_format, opt_ptr_eq, str_from_raw_parts};
use crate::buffer::{CursorMovement, MoveLineDirection, RcTextBuffer, TextBuffer, TextBufferCell};
use crate::cell::*;
use crate::clipboard::Clipboard;
use crate::document::WriteableDocument;
@ -158,12 +160,13 @@ use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedC
use crate::hash::*;
use crate::helpers::*;
use crate::input::{InputKeyMod, kbmod, vk};
use crate::{apperr, arena_format, input, simd, unicode};
use crate::oklab::StraightRgba;
use crate::{input, simd, unicode};
const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant
const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT);
const KBMOD_FOR_WORD_NAV: InputKeyMod =
if cfg!(target_os = "macos") { kbmod::ALT } else { kbmod::CTRL };
if cfg!(any(target_os = "macos", target_os = "ios")) { kbmod::ALT } else { kbmod::CTRL };
type Input<'input> = input::Input<'input>;
type InputKey = input::InputKey;
@ -315,10 +318,10 @@ pub struct Tui {
framebuffer: Framebuffer,
modifier_translations: ModifierTranslations,
floater_default_bg: u32,
floater_default_fg: u32,
modal_default_bg: u32,
modal_default_fg: u32,
floater_default_bg: StraightRgba,
floater_default_fg: StraightRgba,
modal_default_bg: StraightRgba,
modal_default_fg: StraightRgba,
/// Last known terminal size.
///
@ -372,7 +375,7 @@ pub struct Tui {
impl Tui {
/// Creates a new [`Tui`] instance for storing state across frames.
pub fn new() -> apperr::Result<Self> {
pub fn new() -> io::Result<Self> {
let arena_prev = Arena::new(128 * MEBI)?;
let arena_next = Arena::new(128 * MEBI)?;
// SAFETY: Since `prev_tree` refers to `arena_prev`/`arena_next`, from its POV the lifetime
@ -391,10 +394,10 @@ impl Tui {
alt: "Alt",
shift: "Shift",
},
floater_default_bg: 0,
floater_default_fg: 0,
modal_default_bg: 0,
modal_default_fg: 0,
floater_default_bg: StraightRgba::zero(),
floater_default_fg: StraightRgba::zero(),
modal_default_bg: StraightRgba::zero(),
modal_default_fg: StraightRgba::zero(),
size: Size { width: 0, height: 0 },
mouse_position: Point::MIN,
@ -425,7 +428,7 @@ impl Tui {
}
/// Sets up the framebuffer's color palette.
pub fn setup_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
pub fn setup_indexed_colors(&mut self, colors: [StraightRgba; INDEXED_COLORS_COUNT]) {
self.framebuffer.set_indexed_colors(colors);
}
@ -435,22 +438,22 @@ impl Tui {
}
/// Set the default background color for floaters (dropdowns, etc.).
pub fn set_floater_default_bg(&mut self, color: u32) {
pub fn set_floater_default_bg(&mut self, color: StraightRgba) {
self.floater_default_bg = color;
}
/// Set the default foreground color for floaters (dropdowns, etc.).
pub fn set_floater_default_fg(&mut self, color: u32) {
pub fn set_floater_default_fg(&mut self, color: StraightRgba) {
self.floater_default_fg = color;
}
/// Set the default background color for modals.
pub fn set_modal_default_bg(&mut self, color: u32) {
pub fn set_modal_default_bg(&mut self, color: StraightRgba) {
self.modal_default_bg = color;
}
/// Set the default foreground color for modals.
pub fn set_modal_default_fg(&mut self, color: u32) {
pub fn set_modal_default_fg(&mut self, color: StraightRgba) {
self.modal_default_fg = color;
}
@ -469,20 +472,25 @@ impl Tui {
/// Returns an indexed color from the framebuffer.
#[inline]
pub fn indexed(&self, index: IndexedColor) -> u32 {
pub fn indexed(&self, index: IndexedColor) -> StraightRgba {
self.framebuffer.indexed(index)
}
/// Returns an indexed color from the framebuffer with the given alpha.
/// See [`Framebuffer::indexed_alpha()`].
#[inline]
pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
pub fn indexed_alpha(
&self,
index: IndexedColor,
numerator: u32,
denominator: u32,
) -> StraightRgba {
self.framebuffer.indexed_alpha(index, numerator, denominator)
}
/// Returns a color in contrast with the given color.
/// See [`Framebuffer::contrasted()`].
pub fn contrasted(&self, color: u32) -> u32 {
pub fn contrasted(&self, color: StraightRgba) -> StraightRgba {
self.framebuffer.contrasted(color)
}
@ -569,6 +577,7 @@ impl Tui {
&& next_state != InputMouseState::None;
let mouse_up = self.mouse_state != InputMouseState::None
&& next_state == InputMouseState::None;
let is_scroll = next_scroll != Point::default();
let is_drag = self.mouse_state == InputMouseState::Left
&& next_state == InputMouseState::Left
&& next_position != self.mouse_position;
@ -607,7 +616,11 @@ impl Tui {
}
}
if mouse_down {
if is_scroll {
next_state = self.mouse_state;
} else if is_drag {
self.mouse_is_drag = true;
} else if mouse_down {
// Transition from no mouse input to some mouse input --> Record the mouse down position.
Self::build_node_path(hovered_node, &mut self.mouse_down_node_path);
@ -662,8 +675,6 @@ impl Tui {
}
self.mouse_up_timestamp = now;
} else if is_drag {
self.mouse_is_drag = true;
}
input_mouse_modifiers = mouse.modifiers;
@ -1172,14 +1183,14 @@ impl Tui {
result.push_str(" bordered: true\r\n");
}
if node.attributes.bg != 0 {
if node.attributes.bg.to_ne() != 0 {
result.push_repeat(' ', depth * 2);
_ = write!(result, " bg: #{:08x}\r\n", node.attributes.bg);
_ = write!(result, " bg: {:?}\r\n", node.attributes.bg);
}
if node.attributes.fg != 0 {
if node.attributes.fg.to_ne() != 0 {
result.push_repeat(' ', depth * 2);
_ = write!(result, " fg: #{:08x}\r\n", node.attributes.fg);
_ = write!(result, " fg: {:?}\r\n", node.attributes.fg);
}
if self.is_node_focused(node.id) {
@ -1359,20 +1370,25 @@ impl<'a> Context<'a, '_> {
/// Returns an indexed color from the framebuffer.
#[inline]
pub fn indexed(&self, index: IndexedColor) -> u32 {
pub fn indexed(&self, index: IndexedColor) -> StraightRgba {
self.tui.framebuffer.indexed(index)
}
/// Returns an indexed color from the framebuffer with the given alpha.
/// See [`Framebuffer::indexed_alpha()`].
#[inline]
pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
pub fn indexed_alpha(
&self,
index: IndexedColor,
numerator: u32,
denominator: u32,
) -> StraightRgba {
self.tui.framebuffer.indexed_alpha(index, numerator, denominator)
}
/// Returns a color in contrast with the given color.
/// See [`Framebuffer::contrasted()`].
pub fn contrasted(&self, color: u32) -> u32 {
pub fn contrasted(&self, color: StraightRgba) -> StraightRgba {
self.tui.framebuffer.contrasted(color)
}
@ -1642,13 +1658,13 @@ impl<'a> Context<'a, '_> {
}
/// Assigns a sRGB background color to the current node.
pub fn attr_background_rgba(&mut self, bg: u32) {
pub fn attr_background_rgba(&mut self, bg: StraightRgba) {
let mut last_node = self.tree.last_node.borrow_mut();
last_node.attributes.bg = bg;
}
/// Assigns a sRGB foreground color to the current node.
pub fn attr_foreground_rgba(&mut self, fg: u32) {
pub fn attr_foreground_rgba(&mut self, fg: StraightRgba) {
let mut last_node = self.tree.last_node.borrow_mut();
last_node.attributes.fg = fg;
}
@ -1927,7 +1943,7 @@ impl<'a> Context<'a, '_> {
}
/// Changes the active pencil color of the current label.
pub fn styled_label_set_foreground(&mut self, fg: u32) {
pub fn styled_label_set_foreground(&mut self, fg: StraightRgba) {
let mut node = self.tree.last_node.borrow_mut();
let NodeContent::Text(content) = &mut node.content else {
unreachable!();
@ -2186,120 +2202,120 @@ impl<'a> Context<'a, '_> {
let mut make_cursor_visible = false;
let mut change_preferred_column = false;
if self.tui.mouse_state != InputMouseState::None
&& self.tui.was_mouse_down_on_node(node_prev.id)
// Scrolling works even if the node isn't focused.
if self.input_scroll_delta != Point::default()
&& node_prev.inner_clipped.contains(self.tui.mouse_position)
{
// Scrolling works even if the node isn't focused.
if self.tui.mouse_state == InputMouseState::Scroll {
tc.scroll_offset.x += self.input_scroll_delta.x;
tc.scroll_offset.y += self.input_scroll_delta.y;
self.set_input_consumed();
} else if self.tui.is_node_focused(node_prev.id) {
let mouse = self.tui.mouse_position;
let inner = node_prev.inner;
let text_rect = Rect {
left: inner.left + tb.margin_width(),
top: inner.top,
right: inner.right - !single_line as CoordType,
bottom: inner.bottom,
};
let track_rect = Rect {
left: text_rect.right,
top: inner.top,
right: inner.right,
bottom: inner.bottom,
};
let pos = Point {
x: mouse.x - inner.left - tb.margin_width() + tc.scroll_offset.x,
y: mouse.y - inner.top + tc.scroll_offset.y,
};
tc.scroll_offset.x += self.input_scroll_delta.x;
tc.scroll_offset.y += self.input_scroll_delta.y;
self.set_input_consumed();
return make_cursor_visible;
} else if self.tui.mouse_state != InputMouseState::None
&& self.tui.is_node_focused(node_prev.id)
{
let mouse = self.tui.mouse_position;
let inner = node_prev.inner;
let text_rect = Rect {
left: inner.left + tb.margin_width(),
top: inner.top,
right: inner.right - !single_line as CoordType,
bottom: inner.bottom,
};
let track_rect = Rect {
left: text_rect.right,
top: inner.top,
right: inner.right,
bottom: inner.bottom,
};
let pos = Point {
x: mouse.x - inner.left - tb.margin_width() + tc.scroll_offset.x,
y: mouse.y - inner.top + tc.scroll_offset.y,
};
if text_rect.contains(self.tui.mouse_down_position) {
if self.tui.mouse_is_drag {
tb.selection_update_visual(pos);
tc.preferred_column = tb.cursor_visual_pos().x;
if text_rect.contains(self.tui.mouse_down_position) {
if self.tui.mouse_is_drag {
tb.selection_update_visual(pos);
tc.preferred_column = tb.cursor_visual_pos().x;
let height = inner.height();
let height = inner.height();
// If the editor is only 1 line tall we can't possibly scroll up or down.
if height >= 2 {
fn calc(min: CoordType, max: CoordType, mouse: CoordType) -> CoordType {
// Otherwise, the scroll zone is up to 3 lines at the top/bottom.
let zone_height = ((max - min) / 2).min(3);
// If the editor is only 1 line tall we can't possibly scroll up or down.
if height >= 2 {
fn calc(min: CoordType, max: CoordType, mouse: CoordType) -> CoordType {
// Otherwise, the scroll zone is up to 3 lines at the top/bottom.
let zone_height = ((max - min) / 2).min(3);
// The .y positions where the scroll zones begin:
// Mouse coordinates above top and below bottom respectively.
let scroll_min = min + zone_height;
let scroll_max = max - zone_height - 1;
// The .y positions where the scroll zones begin:
// Mouse coordinates above top and below bottom respectively.
let scroll_min = min + zone_height;
let scroll_max = max - zone_height - 1;
// Calculate the delta for scrolling up or down.
let delta_min = (mouse - scroll_min).clamp(-zone_height, 0);
let delta_max = (mouse - scroll_max).clamp(0, zone_height);
// Calculate the delta for scrolling up or down.
let delta_min = (mouse - scroll_min).clamp(-zone_height, 0);
let delta_max = (mouse - scroll_max).clamp(0, zone_height);
// If I didn't mess up my logic here, only one of the two values can possibly be !=0.
let idx = 3 + delta_min + delta_max;
// If I didn't mess up my logic here, only one of the two values can possibly be !=0.
let idx = 3 + delta_min + delta_max;
const SPEEDS: [CoordType; 7] = [-9, -3, -1, 0, 1, 3, 9];
let idx = idx.clamp(0, SPEEDS.len() as CoordType) as usize;
SPEEDS[idx]
}
let delta_x = calc(text_rect.left, text_rect.right, mouse.x);
let delta_y = calc(text_rect.top, text_rect.bottom, mouse.y);
tc.scroll_offset.x += delta_x;
tc.scroll_offset.y += delta_y;
if delta_x != 0 || delta_y != 0 {
self.tui.read_timeout = time::Duration::from_millis(25);
}
const SPEEDS: [CoordType; 7] = [-9, -3, -1, 0, 1, 3, 9];
let idx = idx.clamp(0, SPEEDS.len() as CoordType) as usize;
SPEEDS[idx]
}
} else {
match self.input_mouse_click {
5.. => {}
4 => tb.select_all(),
3 => tb.select_line(),
2 => tb.select_word(),
_ => match self.tui.mouse_state {
InputMouseState::Left => {
if self.input_mouse_modifiers.contains(kbmod::SHIFT) {
// TODO: Untested because Windows Terminal surprisingly doesn't support Shift+Click.
tb.selection_update_visual(pos);
} else {
tb.cursor_move_to_visual(pos);
}
tc.preferred_column = tb.cursor_visual_pos().x;
make_cursor_visible = true;
}
_ => return false,
},
let delta_x = calc(text_rect.left, text_rect.right, mouse.x);
let delta_y = calc(text_rect.top, text_rect.bottom, mouse.y);
tc.scroll_offset.x += delta_x;
tc.scroll_offset.y += delta_y;
if delta_x != 0 || delta_y != 0 {
self.tui.read_timeout = time::Duration::from_millis(25);
}
}
} else if track_rect.contains(self.tui.mouse_down_position) {
if self.tui.mouse_state == InputMouseState::Release {
tc.scroll_offset_y_drag_start = CoordType::MIN;
} else if self.tui.mouse_is_drag {
if tc.scroll_offset_y_drag_start == CoordType::MIN {
tc.scroll_offset_y_drag_start = tc.scroll_offset.y;
}
// The textarea supports 1 height worth of "scrolling beyond the end".
// `track_height` is the same as the viewport height.
let scrollable_height = tb.visual_line_count() - 1;
if scrollable_height > 0 {
let trackable = track_rect.height() - tc.thumb_height;
let delta_y = mouse.y - self.tui.mouse_down_position.y;
tc.scroll_offset.y = tc.scroll_offset_y_drag_start
+ (delta_y as i64 * scrollable_height as i64 / trackable as i64)
as CoordType;
}
} else {
match self.input_mouse_click {
5.. => {}
4 => tb.select_all(),
3 => tb.select_line(),
2 => tb.select_word(),
_ => match self.tui.mouse_state {
InputMouseState::Left => {
if self.input_mouse_modifiers.contains(kbmod::SHIFT) {
// TODO: Untested because Windows Terminal surprisingly doesn't support Shift+Click.
tb.selection_update_visual(pos);
} else {
tb.cursor_move_to_visual(pos);
}
tc.preferred_column = tb.cursor_visual_pos().x;
make_cursor_visible = true;
}
_ => return false,
},
}
}
} else if track_rect.contains(self.tui.mouse_down_position) {
if self.tui.mouse_state == InputMouseState::Release {
tc.scroll_offset_y_drag_start = CoordType::MIN;
} else if self.tui.mouse_is_drag {
if tc.scroll_offset_y_drag_start == CoordType::MIN {
tc.scroll_offset_y_drag_start = tc.scroll_offset.y;
}
self.set_input_consumed();
// The textarea supports 1 height worth of "scrolling beyond the end".
// `track_height` is the same as the viewport height.
let scrollable_height = tb.visual_line_count() - 1;
if scrollable_height > 0 {
let trackable = track_rect.height() - tc.thumb_height;
let delta_y = mouse.y - self.tui.mouse_down_position.y;
tc.scroll_offset.y = tc.scroll_offset_y_drag_start
+ (delta_y as i64 * scrollable_height as i64 / trackable as i64)
as CoordType;
}
}
}
self.set_input_consumed();
return make_cursor_visible;
}
@ -2331,11 +2347,7 @@ impl<'a> Context<'a, '_> {
// If this is just a simple input field, don't consume Tab (= early return).
return false;
}
if modifiers == kbmod::SHIFT {
tb.unindent();
} else {
write = b"\t";
}
tb.indent_change(if modifiers == kbmod::SHIFT { -1 } else { 1 });
}
vk::RETURN => {
if single_line {
@ -2547,6 +2559,7 @@ impl<'a> Context<'a, '_> {
y: tb.cursor_visual_pos().y - 1,
});
}
kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Up),
kbmod::CTRL_ALT => {
// TODO: Add cursor above
}
@ -2617,6 +2630,7 @@ impl<'a> Context<'a, '_> {
tc.preferred_column = tb.cursor_visual_pos().x;
}
}
kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Down),
kbmod::CTRL_ALT => {
// TODO: Add cursor above
}
@ -2638,7 +2652,7 @@ impl<'a> Context<'a, '_> {
_ => return false,
},
vk::B => match modifiers {
kbmod::ALT if cfg!(target_os = "macos") => {
kbmod::ALT if cfg!(any(target_os = "macos", target_os = "ios")) => {
// On macOS, terminals commonly emit the Emacs style
// Alt+B (ESC b) sequence for Alt+Left.
tb.cursor_move_delta(CursorMovement::Word, -1);
@ -2646,7 +2660,7 @@ impl<'a> Context<'a, '_> {
_ => return false,
},
vk::F => match modifiers {
kbmod::ALT if cfg!(target_os = "macos") => {
kbmod::ALT if cfg!(any(target_os = "macos", target_os = "ios")) => {
// On macOS, terminals commonly emit the Emacs style
// Alt+F (ESC f) sequence for Alt+Right.
tb.cursor_move_delta(CursorMovement::Word, 1);
@ -2657,6 +2671,10 @@ impl<'a> Context<'a, '_> {
kbmod::CTRL => tb.delete(CursorMovement::Word, -1),
_ => return false,
},
vk::L => match modifiers {
kbmod::CTRL => tb.select_line(),
_ => return false,
},
vk::X => match modifiers {
kbmod::CTRL => tb.cut(self.clipboard_mut()),
_ => return false,
@ -2804,9 +2822,15 @@ impl<'a> Context<'a, '_> {
}
if !self.input_consumed {
if self.tui.mouse_state != InputMouseState::None {
let container_rect = prev_container.inner;
let container_rect = prev_container.inner;
if self.input_scroll_delta != Point::default()
&& container_rect.contains(self.tui.mouse_position)
{
sc.scroll_offset.x += self.input_scroll_delta.x;
sc.scroll_offset.y += self.input_scroll_delta.y;
self.set_input_consumed();
} else if self.tui.mouse_state != InputMouseState::None {
match self.tui.mouse_state {
InputMouseState::Left => {
if self.tui.mouse_is_drag {
@ -2846,13 +2870,6 @@ impl<'a> Context<'a, '_> {
InputMouseState::Release => {
sc.scroll_offset_y_drag_start = CoordType::MIN;
}
InputMouseState::Scroll => {
if container_rect.contains(self.tui.mouse_position) {
sc.scroll_offset.x += self.input_scroll_delta.x;
sc.scroll_offset.y += self.input_scroll_delta.y;
self.set_input_consumed();
}
}
_ => {}
}
} else if self.tui.is_subtree_focused_alt(container_id, container_depth)
@ -3103,7 +3120,8 @@ impl<'a> Context<'a, '_> {
///
/// Returns true if the menu is open. Continue appending items to it in that case.
pub fn menubar_menu_begin(&mut self, text: &str, accelerator: char) -> bool {
let accelerator = if cfg!(target_os = "macos") { '\0' } else { accelerator };
let accelerator =
if cfg!(any(target_os = "macos", target_os = "ios")) { '\0' } else { accelerator };
let mixin = self.tree.current_node.borrow().child_count as u64;
self.next_block_id_mixin(mixin);
@ -3584,7 +3602,7 @@ impl<'a> NodeMap<'a> {
}
/// Gets a node by its ID.
fn get(&mut self, id: u64) -> Option<&'a NodeCell<'a>> {
fn get(&self, id: u64) -> Option<&'a NodeCell<'a>> {
let shift = self.shift;
let mask = self.mask;
let mut slot = id >> shift;
@ -3614,8 +3632,8 @@ struct NodeAttributes {
float: Option<FloatAttributes>,
position: Position,
padding: Rect,
bg: u32,
fg: u32,
bg: StraightRgba,
fg: StraightRgba,
reverse: bool,
bordered: bool,
focusable: bool,
@ -3639,12 +3657,12 @@ struct TableContent<'a> {
/// NOTE: Must not contain items that require drop().
struct StyledTextChunk {
offset: usize,
fg: u32,
fg: StraightRgba,
attr: Attributes,
}
const INVALID_STYLED_TEXT_CHUNK: StyledTextChunk =
StyledTextChunk { offset: usize::MAX, fg: 0, attr: Attributes::None };
StyledTextChunk { offset: usize::MAX, fg: StraightRgba::zero(), attr: Attributes::None };
/// NOTE: Must not contain items that require drop().
struct TextContent<'a> {

View File

@ -128,17 +128,13 @@ impl<'input> Stream<'_, 'input> {
self.off
}
/// Reads and consumes raw bytes from the input.
pub fn read(&mut self, dst: &mut [u8]) -> usize {
let bytes = self.input.as_bytes();
let off = self.off.min(bytes.len());
let len = dst.len().min(bytes.len() - off);
dst[..len].copy_from_slice(&bytes[off..off + len]);
self.off += len;
len
/// Returns `true` if the input has been fully parsed.
pub fn done(&self) -> bool {
self.off >= self.input.len()
}
fn decode_next(&mut self) -> char {
/// Decodes and consumes the next UTF-8 character from the input.
pub fn next_char(&mut self) -> char {
let mut iter = Utf8Chars::new(self.input.as_bytes(), self.off);
let c = iter.next().unwrap_or('\0');
self.off = iter.offset();
@ -190,7 +186,7 @@ impl<'input> Stream<'_, 'input> {
return Some(Token::Text(&input[beg..self.off]));
}
},
State::Esc => match self.decode_next() {
State::Esc => match self.next_char() {
'[' => {
self.parser.state = State::Csi;
self.parser.csi.private_byte = '\0';
@ -216,7 +212,7 @@ impl<'input> Stream<'_, 'input> {
},
State::Ss3 => {
self.parser.state = State::Ground;
return Some(Token::SS3(self.decode_next()));
return Some(Token::SS3(self.next_char()));
}
State::Csi => {
loop {

14
crates/stdext/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "stdext"
version = "0.0.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[features]
single-threaded = []
[target.'cfg(unix)'.dependencies]
libc = "0.2"

View File

@ -4,11 +4,11 @@
#![allow(clippy::missing_safety_doc, clippy::mut_from_ref)]
use std::alloc::{AllocError, Allocator, Layout};
use std::mem::{self, MaybeUninit};
use std::io;
use std::mem::MaybeUninit;
use std::ptr::NonNull;
use super::release;
use crate::apperr;
/// A debug wrapper for [`release::Arena`].
///
@ -19,7 +19,7 @@ use crate::apperr;
/// It is completely valid for the same [`release::Arena`] to be borrowed multiple times at once,
/// *as long as* you only use the most recent borrow. Bad example:
/// ```should_panic
/// use edit::arena::scratch_arena;
/// use stdext::arena::scratch_arena;
///
/// let mut scratch1 = scratch_arena(None);
/// let mut scratch2 = scratch_arena(None);
@ -63,14 +63,14 @@ impl Arena {
Self::Owned { arena: release::Arena::empty() }
}
pub fn new(capacity: usize) -> apperr::Result<Self> {
pub fn new(capacity: usize) -> io::Result<Self> {
Ok(Self::Owned { arena: release::Arena::new(capacity)? })
}
pub(super) fn delegated(delegate: &release::Arena) -> Self {
let borrow = delegate.borrows.get() + 1;
delegate.borrows.set(borrow);
Self::Delegated { delegate: unsafe { mem::transmute(delegate) }, borrow }
Self::Delegated { delegate: unsafe { &*(delegate as *const _) }, borrow }
}
#[inline]
@ -114,7 +114,7 @@ impl Arena {
unsafe impl Allocator for Arena {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
self.delegate_target().alloc_raw(layout.size(), layout.align())
Ok(self.delegate_target().alloc_raw(layout.size(), layout.align()))
}
fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {

View File

@ -0,0 +1,55 @@
use std::fs::File;
use std::io::{self, Read};
use std::mem::MaybeUninit;
use std::path::Path;
use std::slice::from_raw_parts_mut;
use super::{Arena, ArenaString};
pub fn read_to_vec<P: AsRef<Path>>(arena: &Arena, path: P) -> io::Result<Vec<u8, &Arena>> {
fn inner<'a>(arena: &'a Arena, path: &Path) -> io::Result<Vec<u8, &'a Arena>> {
let mut file = File::open(path)?;
let mut vec = Vec::new_in(arena);
const MIN_SIZE: usize = 1024;
const MAX_SIZE: usize = 128 * 1024;
let mut buf_size = MIN_SIZE;
loop {
vec.reserve(buf_size);
let spare = vec.spare_capacity_mut();
let to_read = spare.len().min(buf_size);
match file_read_uninit(&mut file, &mut spare[..to_read]) {
Ok(0) => break,
Ok(n) => {
unsafe { vec.set_len(vec.len() + n) };
buf_size = (buf_size * 2).min(MAX_SIZE);
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(vec)
}
inner(arena, path.as_ref())
}
pub fn read_to_string<P: AsRef<Path>>(arena: &Arena, path: P) -> io::Result<ArenaString<'_>> {
fn inner<'a>(arena: &'a Arena, path: &Path) -> io::Result<ArenaString<'a>> {
let vec = read_to_vec(arena, path)?;
ArenaString::from_utf8(vec).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "stream did not contain valid UTF-8")
})
}
inner(arena, path.as_ref())
}
fn file_read_uninit<T: Read>(file: &mut T, buf: &mut [MaybeUninit<u8>]) -> io::Result<usize> {
unsafe {
let buf_slice = from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len());
let n = file.read(buf_slice)?;
Ok(n)
}
}

View File

@ -5,13 +5,15 @@
#[cfg(debug_assertions)]
mod debug;
mod fs;
mod release;
mod scratch;
mod string;
#[cfg(all(not(doc), debug_assertions))]
pub use self::debug::Arena;
pub use self::debug::*;
pub use self::fs::*;
#[cfg(any(doc, not(debug_assertions)))]
pub use self::release::Arena;
pub use self::scratch::{ScratchArena, init, scratch_arena};
pub use self::string::ArenaString;
pub use self::release::*;
pub use self::scratch::*;
pub use self::string::*;

View File

@ -5,15 +5,16 @@
use std::alloc::{AllocError, Allocator, Layout};
use std::cell::Cell;
use std::hint::cold_path;
use std::mem::MaybeUninit;
use std::ptr::{self, NonNull};
use std::{mem, slice};
use std::{io, mem, slice};
use crate::helpers::*;
use crate::{apperr, sys};
use crate::{cold_path, sys};
const ALLOC_CHUNK_SIZE: usize = 64 * KIBI;
#[cfg(target_pointer_width = "32")]
const ALLOC_CHUNK_SIZE: usize = 32 * 1024;
#[cfg(target_pointer_width = "64")]
const ALLOC_CHUNK_SIZE: usize = 64 * 1024;
/// An arena allocator.
///
@ -64,7 +65,7 @@ impl Arena {
}
}
pub fn new(capacity: usize) -> apperr::Result<Self> {
pub fn new(capacity: usize) -> io::Result<Self> {
let capacity = (capacity.max(1) + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
let base = unsafe { sys::virtual_reserve(capacity)? };
@ -79,6 +80,10 @@ impl Arena {
})
}
pub fn is_empty(&self) -> bool {
self.base == NonNull::dangling()
}
pub fn offset(&self) -> usize {
self.offset.get()
}
@ -101,11 +106,7 @@ impl Arena {
}
#[inline]
pub(super) fn alloc_raw(
&self,
bytes: usize,
alignment: usize,
) -> Result<NonNull<[u8]>, AllocError> {
pub(super) fn alloc_raw(&self, bytes: usize, alignment: usize) -> NonNull<[u8]> {
let commit = self.commit.get();
let offset = self.offset.get();
@ -123,12 +124,12 @@ impl Arena {
}
self.offset.replace(end);
Ok(unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), bytes) })
unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), bytes) }
}
// With the code in `alloc_raw_bump()` out of the way, `alloc_raw()` compiles down to some super tight assembly.
#[cold]
fn alloc_raw_bump(&self, beg: usize, end: usize) -> Result<NonNull<[u8]>, AllocError> {
fn alloc_raw_bump(&self, beg: usize, end: usize) -> NonNull<[u8]> {
let offset = self.offset.get();
let commit_old = self.commit.get();
let commit_new = (end + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
@ -138,7 +139,10 @@ impl Arena {
sys::virtual_commit(self.base.add(commit_old), commit_new - commit_old).is_err()
}
{
return Err(AllocError);
// Panicking inside this [cold] function has the benefit of removing duplicated panic code from any
// inlined alloc() function. If we ever add fallible allocations, we should probably duplicate alloc_raw()
// and alloc_raw_bump() instead of returning a Result here and calling unwrap() in the common path.
panic!("out of memory");
}
if cfg!(debug_assertions) {
@ -149,29 +153,31 @@ impl Arena {
self.commit.replace(commit_new);
self.offset.replace(end);
Ok(unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), end - beg) })
unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), end - beg) }
}
#[inline]
#[allow(clippy::mut_from_ref)]
pub fn alloc_uninit<T>(&self) -> &mut MaybeUninit<T> {
let bytes = mem::size_of::<T>();
let alignment = mem::align_of::<T>();
let ptr = self.alloc_raw(bytes, alignment).unwrap();
let ptr = self.alloc_raw(bytes, alignment);
unsafe { ptr.cast().as_mut() }
}
#[inline]
#[allow(clippy::mut_from_ref)]
pub fn alloc_uninit_slice<T>(&self, count: usize) -> &mut [MaybeUninit<T>] {
let bytes = mem::size_of::<T>() * count;
let alignment = mem::align_of::<T>();
let ptr = self.alloc_raw(bytes, alignment).unwrap();
let ptr = self.alloc_raw(bytes, alignment);
unsafe { slice::from_raw_parts_mut(ptr.cast().as_ptr(), count) }
}
}
impl Drop for Arena {
fn drop(&mut self) {
if self.base != NonNull::dangling() {
if !self.is_empty() {
unsafe { sys::virtual_release(self.base, self.capacity) };
}
}
@ -185,11 +191,11 @@ impl Default for Arena {
unsafe impl Allocator for Arena {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
self.alloc_raw(layout.size(), layout.align())
Ok(self.alloc_raw(layout.size(), layout.align()))
}
fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
let p = self.alloc_raw(layout.size(), layout.align())?;
let p = self.alloc_raw(layout.size(), layout.align());
unsafe { p.cast::<u8>().as_ptr().write_bytes(0, p.len()) }
Ok(p)
}
@ -215,7 +221,7 @@ unsafe impl Allocator for Arena {
let delta = new_layout.size() - old_layout.size();
// Assuming that the given ptr/length area is at the end of the arena,
// we can just push more memory to the end of the arena to grow it.
self.alloc_raw(delta, 1)?;
self.alloc_raw(delta, 1);
} else {
cold_path();

View File

@ -0,0 +1,170 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::io;
use std::ops::Deref;
#[cfg(debug_assertions)]
use super::debug;
use super::{Arena, release};
use crate::helpers::*;
/// Borrows an [`Arena`] for temporary allocations.
///
/// See [`scratch_arena`].
#[cfg(debug_assertions)]
pub struct ScratchArena<'a> {
arena: debug::Arena,
offset: usize,
_phantom: std::marker::PhantomData<&'a ()>,
}
#[cfg(not(debug_assertions))]
pub struct ScratchArena<'a> {
arena: &'a Arena,
offset: usize,
}
#[cfg(debug_assertions)]
impl<'a> ScratchArena<'a> {
fn new(arena: &'a release::Arena) -> Self {
let offset = arena.offset();
ScratchArena { arena: Arena::delegated(arena), _phantom: std::marker::PhantomData, offset }
}
}
#[cfg(not(debug_assertions))]
impl<'a> ScratchArena<'a> {
fn new(arena: &'a release::Arena) -> Self {
let offset = arena.offset();
ScratchArena { arena, offset }
}
}
impl Drop for ScratchArena<'_> {
fn drop(&mut self) {
unsafe { self.arena.reset(self.offset) };
}
}
#[cfg(debug_assertions)]
impl Deref for ScratchArena<'_> {
type Target = debug::Arena;
fn deref(&self) -> &Self::Target {
&self.arena
}
}
#[cfg(not(debug_assertions))]
impl Deref for ScratchArena<'_> {
type Target = Arena;
fn deref(&self) -> &Self::Target {
self.arena
}
}
mod single_threaded {
use super::*;
static mut S_SCRATCH: [release::Arena; 2] =
const { [release::Arena::empty(), release::Arena::empty()] };
/// Initialize the scratch arenas with a given capacity.
/// Call this before using [`scratch_arena`].
#[allow(dead_code)]
pub fn init(capacity: usize) -> io::Result<()> {
unsafe {
for s in &mut S_SCRATCH[..] {
*s = release::Arena::new(capacity)?;
}
}
Ok(())
}
/// Need an arena for temporary allocations? [`scratch_arena`] got you covered.
/// Call [`scratch_arena`] and it'll return an [`Arena`] that resets when it goes out of scope.
///
/// ---
///
/// Most methods make just two kinds of allocations:
/// * Interior: Temporary data that can be deallocated when the function returns.
/// * Exterior: Data that is returned to the caller and must remain alive until the caller stops using it.
///
/// Such methods only have two lifetimes, for which you consequently also only need two arenas.
/// ...even if your method calls other methods recursively! This is because the exterior allocations
/// of a callee are simply interior allocations to the caller, and so on, recursively.
///
/// This works as long as the two arenas flip/flop between being used as interior/exterior allocator
/// along the callstack. To ensure that is the case, we use a recursion counter in debug builds.
///
/// This approach was described among others at: <https://nullprogram.com/blog/2023/09/27/>
///
/// # Safety
///
/// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`.
#[allow(dead_code)]
pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> {
unsafe {
#[cfg(debug_assertions)]
let conflict = conflict.map(|a| a.delegate_target_unchecked());
let index = opt_ptr_eq(conflict, Some(&S_SCRATCH[0])) as usize;
let arena = &S_SCRATCH[index];
ScratchArena::new(arena)
}
}
}
mod multi_threaded {
use std::cell::Cell;
use std::ptr;
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
thread_local! {
static S_SCRATCH: [Cell<release::Arena>; 2] =
const { [Cell::new(release::Arena::empty()), Cell::new(release::Arena::empty())] };
}
static INIT_SIZE: AtomicUsize = AtomicUsize::new(128 * MEBI);
/// Sets the default scratch arena size.
pub fn init(capacity: usize) -> io::Result<()> {
if capacity != 0 {
INIT_SIZE.store(capacity, Ordering::Relaxed);
}
Ok(())
}
/// See `single_threaded::scratch_arena`.
#[allow(dead_code)]
pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> {
#[cfg(debug_assertions)]
let conflict = conflict.map(|a| a.delegate_target_unchecked());
#[cold]
fn init(s: &[Cell<release::Arena>; 2]) {
let capacity = INIT_SIZE.load(Ordering::Relaxed);
for s in s {
s.set(release::Arena::new(capacity).unwrap());
}
}
S_SCRATCH.with(|arenas| {
let index = ptr::eq(opt_ptr(conflict), arenas[0].as_ptr()) as usize;
let arena = unsafe { &*arenas[index].as_ptr() };
if arena.is_empty() {
init(arenas);
}
ScratchArena::new(arena)
})
}
}
#[cfg(not(feature = "single-threaded"))]
pub use multi_threaded::*;
#[cfg(feature = "single-threaded")]
pub use single_threaded::*;

View File

@ -3,6 +3,7 @@
use std::fmt;
use std::ops::{Bound, Deref, DerefMut, RangeBounds};
use std::str::Utf8Error;
use super::Arena;
use crate::helpers::*;
@ -35,6 +36,11 @@ impl<'a> ArenaString<'a> {
res
}
pub fn from_utf8(vec: Vec<u8, &'a Arena>) -> Result<Self, Utf8Error> {
str::from_utf8(&vec)?;
Ok(Self { vec })
}
/// It says right here that you checked if `bytes` is valid UTF-8
/// and you are sure it is. Presto! Here's an `ArenaString`!
///
@ -90,6 +96,13 @@ impl<'a> ArenaString<'a> {
}
}
#[must_use]
pub fn from_iter<T: IntoIterator<Item = char>>(arena: &'a Arena, iter: T) -> Self {
let mut s = Self::new_in(arena);
s.extend(iter);
s
}
/// It's empty.
pub fn is_empty(&self) -> bool {
self.vec.is_empty()
@ -120,6 +133,10 @@ impl<'a> ArenaString<'a> {
self.vec.as_slice()
}
pub fn leak(self) -> &'a str {
unsafe { str::from_utf8_unchecked(self.vec.leak()) }
}
/// Returns a mutable reference to the contents of this `String`.
///
/// # Safety
@ -227,12 +244,20 @@ impl fmt::Debug for ArenaString<'_> {
}
}
impl PartialEq<ArenaString<'_>> for ArenaString<'_> {
fn eq(&self, other: &ArenaString) -> bool {
self.as_str() == other.as_str()
}
}
impl PartialEq<&str> for ArenaString<'_> {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl Eq for ArenaString<'_> {}
impl Deref for ArenaString<'_> {
type Target = str;
@ -249,7 +274,7 @@ impl DerefMut for ArenaString<'_> {
impl fmt::Display for ArenaString<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
fmt::Display::fmt(&**self, f)
}
}
@ -267,11 +292,23 @@ impl fmt::Write for ArenaString<'_> {
}
}
impl Extend<char> for ArenaString<'_> {
fn extend<I: IntoIterator<Item = char>>(&mut self, iter: I) {
let iterator = iter.into_iter();
let (lower_bound, _) = iterator.size_hint();
self.reserve(lower_bound);
iterator.for_each(move |c| self.push(c));
}
// TODO: This is where I'd put `extend_one` and `extend_reserve` impls, *but as always*,
// essential stdlib functions are unstable and that means we can't have them.
}
#[macro_export]
macro_rules! arena_format {
($arena:expr, $($arg:tt)*) => {{
use std::fmt::Write as _;
let mut output = $crate::arena::ArenaString::new_in($arena);
let mut output = stdext::arena::ArenaString::new_in($arena);
output.write_fmt(format_args!($($arg)*)).unwrap();
output
}}

View File

@ -4,14 +4,10 @@
//! Random assortment of helpers I didn't know where to put.
use std::alloc::Allocator;
use std::cmp::Ordering;
use std::io::Read;
use std::mem::{self, MaybeUninit};
use std::ops::{Bound, Range, RangeBounds};
use std::{fmt, ptr, slice, str};
use crate::apperr;
pub const KILO: usize = 1000;
pub const MEGA: usize = 1000 * 1000;
pub const GIGA: usize = 1000 * 1000 * 1000;
@ -40,115 +36,9 @@ impl fmt::Display for MetricFormatter<usize> {
}
}
/// A viewport coordinate type used throughout the application.
pub type CoordType = isize;
/// To avoid overflow issues because you're adding two [`CoordType::MAX`]
/// values together, you can use [`COORD_TYPE_SAFE_MAX`] instead.
///
/// It equates to half the bits contained in [`CoordType`], which
/// for instance is 32767 (0x7FFF) when [`CoordType`] is a [`i32`].
pub const COORD_TYPE_SAFE_MAX: CoordType = (1 << (CoordType::BITS / 2 - 1)) - 1;
/// A 2D point. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Point {
pub x: CoordType,
pub y: CoordType,
}
impl Point {
pub const MIN: Self = Self { x: CoordType::MIN, y: CoordType::MIN };
pub const MAX: Self = Self { x: CoordType::MAX, y: CoordType::MAX };
}
impl PartialOrd<Self> for Point {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Point {
fn cmp(&self, other: &Self) -> Ordering {
self.y.cmp(&other.y).then(self.x.cmp(&other.x))
}
}
/// A 2D size. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Size {
pub width: CoordType,
pub height: CoordType,
}
impl Size {
pub fn as_rect(&self) -> Rect {
Rect { left: 0, top: 0, right: self.width, bottom: self.height }
}
}
/// A 2D rectangle. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub left: CoordType,
pub top: CoordType,
pub right: CoordType,
pub bottom: CoordType,
}
impl Rect {
/// Mimics CSS's `padding` property where `padding: a` is `a a a a`.
pub fn one(value: CoordType) -> Self {
Self { left: value, top: value, right: value, bottom: value }
}
/// Mimics CSS's `padding` property where `padding: a b` is `a b a b`,
/// and `a` is top/bottom and `b` is left/right.
pub fn two(top_bottom: CoordType, left_right: CoordType) -> Self {
Self { left: left_right, top: top_bottom, right: left_right, bottom: top_bottom }
}
/// Mimics CSS's `padding` property where `padding: a b c` is `a b c b`,
/// and `a` is top, `b` is left/right, and `c` is bottom.
pub fn three(top: CoordType, left_right: CoordType, bottom: CoordType) -> Self {
Self { left: left_right, top, right: left_right, bottom }
}
/// Is the rectangle empty?
pub fn is_empty(&self) -> bool {
self.left >= self.right || self.top >= self.bottom
}
/// Width of the rectangle.
pub fn width(&self) -> CoordType {
self.right - self.left
}
/// Height of the rectangle.
pub fn height(&self) -> CoordType {
self.bottom - self.top
}
/// Check if it contains a point.
pub fn contains(&self, point: Point) -> bool {
point.x >= self.left && point.x < self.right && point.y >= self.top && point.y < self.bottom
}
/// Intersect two rectangles.
pub fn intersect(&self, rhs: Self) -> Self {
let l = self.left.max(rhs.left);
let t = self.top.max(rhs.top);
let r = self.right.min(rhs.right);
let b = self.bottom.min(rhs.bottom);
// Ensure that the size is non-negative. This avoids bugs,
// because some height/width is negative all of a sudden.
let r = l.max(r);
let b = t.max(b);
Self { left: l, top: t, right: r, bottom: b }
}
}
#[inline(always)]
#[cold]
pub const fn cold_path() {}
/// [`std::cmp::minmax`] is unstable, as per usual.
pub fn minmax<T>(v1: T, v2: T) -> [T; 2]
@ -160,7 +50,7 @@ where
#[inline(always)]
#[allow(clippy::ptr_eq)]
fn opt_ptr<T>(a: Option<&T>) -> *const T {
pub fn opt_ptr<T>(a: Option<&T>) -> *const T {
unsafe { mem::transmute(a) }
}
@ -249,18 +139,6 @@ fn vec_replace_impl<T: Copy, A: Allocator>(dst: &mut Vec<T, A>, range: Range<usi
}
}
/// [`Read`] but with [`MaybeUninit<u8>`] buffers.
pub fn file_read_uninit<T: Read>(
file: &mut T,
buf: &mut [MaybeUninit<u8>],
) -> apperr::Result<usize> {
unsafe {
let buf_slice = slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len());
let n = file.read(buf_slice)?;
Ok(n)
}
}
/// Turns a [`&[u8]`] into a [`&[MaybeUninit<T>]`].
#[inline(always)]
pub const fn slice_as_uninit_ref<T>(slice: &[T]) -> &[MaybeUninit<T>] {

12
crates/stdext/src/lib.rs Normal file
View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Arena allocators. Small and fast.
#![feature(allocator_api)]
pub mod arena;
pub mod sys;
mod helpers;
pub use helpers::*;

View File

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Platform abstractions.
#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;
#[cfg(not(windows))]
pub use std::fs::canonicalize;
#[cfg(unix)]
pub use unix::*;
#[cfg(windows)]
pub use windows::*;

View File

@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::c_int;
use std::io;
use std::ptr::{self, NonNull, null_mut};
/// Reserves a virtual memory region of the given size.
/// To commit the memory, use `virtual_commit`.
/// To release the memory, use `virtual_release`.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Don't forget to release the memory when you're done with it or you'll leak it.
pub unsafe fn virtual_reserve(size: usize) -> io::Result<NonNull<u8>> {
unsafe {
let ptr = libc::mmap(
null_mut(),
size,
desired_mprotect(libc::PROT_READ | libc::PROT_WRITE),
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
);
if ptr.is_null() || ptr::eq(ptr, libc::MAP_FAILED) {
Err(io::Error::last_os_error())
} else {
Ok(NonNull::new_unchecked(ptr as *mut u8))
}
}
}
#[cfg(target_os = "netbsd")]
const fn desired_mprotect(flags: c_int) -> c_int {
// NetBSD allows an mmap(2) caller to specify what protection flags they
// will use later via mprotect. It does not allow a caller to move from
// PROT_NONE to PROT_READ | PROT_WRITE.
//
// see PROT_MPROTECT in man 2 mmap
flags << 3
}
#[cfg(not(target_os = "netbsd"))]
const fn desired_mprotect(_: c_int) -> c_int {
libc::PROT_NONE
}
/// Releases a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from `virtual_reserve`.
pub unsafe fn virtual_release(base: NonNull<u8>, size: usize) {
unsafe {
libc::munmap(base.cast().as_ptr(), size);
}
}
/// Commits a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from `virtual_reserve`
/// and to pass a size less than or equal to the size passed to `virtual_reserve`.
pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> io::Result<()> {
unsafe {
let status = libc::mprotect(base.cast().as_ptr(), size, libc::PROT_READ | libc::PROT_WRITE);
if status != 0 { Err(io::Error::last_os_error()) } else { Ok(()) }
}
}

View File

@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::io;
use std::ptr::{NonNull, null_mut};
const MEM_COMMIT: u32 = 0x00001000;
const MEM_RELEASE: u32 = 0x00008000;
const MEM_RESERVE: u32 = 0x00002000;
const PAGE_READWRITE: u32 = 0x04;
unsafe extern "system" {
fn VirtualAlloc(
lpAddress: *mut u8,
dwSize: usize,
flAllocationType: u32,
flProtect: u32,
) -> *mut u8;
fn VirtualFree(lpAddress: *mut u8, dwSize: usize, dwFreeType: u32) -> i32;
}
/// Reserves a virtual memory region of the given size.
/// To commit the memory, use [`virtual_commit`].
/// To release the memory, use [`virtual_release`].
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Don't forget to release the memory when you're done with it or you'll leak it.
pub unsafe fn virtual_reserve(size: usize) -> io::Result<NonNull<u8>> {
unsafe {
let res = VirtualAlloc(null_mut(), size, MEM_RESERVE, PAGE_READWRITE);
if res.is_null() {
Err(io::Error::last_os_error())
} else {
Ok(NonNull::new_unchecked(res))
}
}
}
/// Releases a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`].
pub unsafe fn virtual_release(base: NonNull<u8>, _size: usize) {
unsafe {
// NOTE: `VirtualFree` fails if the pointer isn't
// a valid base address or if the size isn't zero.
VirtualFree(base.as_ptr() as *mut _, 0, MEM_RELEASE);
}
}
/// Commits a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`]
/// and to pass a size less than or equal to the size passed to [`virtual_reserve`].
pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> io::Result<()> {
unsafe {
let res = VirtualAlloc(base.as_ptr() as *mut _, size, MEM_COMMIT, PAGE_READWRITE);
if res.is_null() { Err(io::Error::last_os_error()) } else { Ok(()) }
}
}

View File

@ -0,0 +1,16 @@
[package]
name = "unicode-gen"
version = "0.0.0"
edition.workspace = true
license.workspace = true
repository.workspace = true
rust-version.workspace = true
[dependencies]
anyhow = "1.0"
chrono = "0.4"
indoc = "2.0"
pico-args = { version = "0.5", features = ["eq-separator"] }
rayon = "1.10"
roxmltree = { version = "0.21", default-features = false, features = ["std"] }

View File

@ -805,7 +805,7 @@ fn extract_values_from_ucd(doc: &roxmltree::Document, out: &Output) -> anyhow::R
"LV" => ClusterBreak::HangulLV, // Hangul Syllable Type LV
"LVT" => ClusterBreak::HangulLVT, // Hangul Syllable Type LVT
_ => bail!(
"Unrecognized GCB {:?} for U+{:04X} to U+{:04X}",
"Unrecognized GCB={} for U+{:04X} to U+{:04X}",
char_attributes.grapheme_cluster_break,
range.start(),
range.end()
@ -818,7 +818,7 @@ fn extract_values_from_ucd(doc: &roxmltree::Document, out: &Output) -> anyhow::R
// and treat it as an alias of EXTEND, but with the special GB11 properties.
if cb != ClusterBreak::Other {
bail!(
"Unexpected GCB {:?} with ExtPict=Y for U+{:04X} to U+{:04X}",
"Unexpected GCB={} with ExtPict=Y for U+{:04X} to U+{:04X}",
char_attributes.grapheme_cluster_break,
range.start(),
range.end()
@ -828,24 +828,37 @@ fn extract_values_from_ucd(doc: &roxmltree::Document, out: &Output) -> anyhow::R
cb = ClusterBreak::ExtPic;
}
cb = match char_attributes.indic_conjunct_break {
"None" | "Extend" => cb,
"Linker" => ClusterBreak::InCBLinker,
"Consonant" => ClusterBreak::InCBConsonant,
_ => bail!(
"Unrecognized InCB {:?} for U+{:04X} to U+{:04X}",
char_attributes.indic_conjunct_break,
range.start(),
range.end()
),
};
if !matches!(char_attributes.indic_conjunct_break, "None" | "Extend") {
// If it's not None/Extend, it's Linker/Consonant, and currently
// all of them are GCB=EX/XX. Since we treat them almost like extenders,
// we need to revisit our assumptions if this ever changes.
if !matches!(cb, ClusterBreak::Other | ClusterBreak::Extend) {
bail!(
"Unexpected GCB={} with InCB={} for U+{:04X} to U+{:04X}",
char_attributes.grapheme_cluster_break,
char_attributes.indic_conjunct_break,
range.start(),
range.end()
);
}
cb = match char_attributes.indic_conjunct_break {
"Linker" => ClusterBreak::InCBLinker,
"Consonant" => ClusterBreak::InCBConsonant,
_ => bail!(
"Unrecognized InCB={} for U+{:04X} to U+{:04X}",
char_attributes.indic_conjunct_break,
range.start(),
range.end()
),
};
}
let mut cw = match char_attributes.east_asian {
"N" | "Na" | "H" => CharacterWidth::Narrow, // Half-width, Narrow, Neutral
"F" | "W" => CharacterWidth::Wide, // Wide, Full-width
"A" => ambiguous_value, // Ambiguous
_ => bail!(
"Unrecognized ea {:?} for U+{:04X} to U+{:04X}",
"Unrecognized ea={} for U+{:04X} to U+{:04X}",
char_attributes.east_asian,
range.start(),
range.end()

1934
i18n/edit.toml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Provides a transparent error type for edit.
use std::{io, result};
use crate::sys;
pub const APP_ICU_MISSING: Error = Error::new_app(0);
/// Edit's transparent `Result` type.
pub type Result<T> = result::Result<T, Error>;
/// Edit's transparent `Error` type.
/// Abstracts over system and application errors.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
App(u32),
Icu(u32),
Sys(u32),
}
impl Error {
pub const fn new_app(code: u32) -> Self {
Self::App(code)
}
pub const fn new_icu(code: u32) -> Self {
Self::Icu(code)
}
pub const fn new_sys(code: u32) -> Self {
Self::Sys(code)
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
sys::io_error_to_apperr(err)
}
}

View File

@ -1,112 +0,0 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ops::Deref;
#[cfg(debug_assertions)]
use super::debug;
use super::{Arena, release};
use crate::apperr;
use crate::helpers::*;
static mut S_SCRATCH: [release::Arena; 2] =
const { [release::Arena::empty(), release::Arena::empty()] };
/// Initialize the scratch arenas with a given capacity.
/// Call this before using [`scratch_arena`].
pub fn init(capacity: usize) -> apperr::Result<()> {
unsafe {
for s in &mut S_SCRATCH[..] {
*s = release::Arena::new(capacity)?;
}
}
Ok(())
}
/// Need an arena for temporary allocations? [`scratch_arena`] got you covered.
/// Call [`scratch_arena`] and it'll return an [`Arena`] that resets when it goes out of scope.
///
/// ---
///
/// Most methods make just two kinds of allocations:
/// * Interior: Temporary data that can be deallocated when the function returns.
/// * Exterior: Data that is returned to the caller and must remain alive until the caller stops using it.
///
/// Such methods only have two lifetimes, for which you consequently also only need two arenas.
/// ...even if your method calls other methods recursively! This is because the exterior allocations
/// of a callee are simply interior allocations to the caller, and so on, recursively.
///
/// This works as long as the two arenas flip/flop between being used as interior/exterior allocator
/// along the callstack. To ensure that is the case, we use a recursion counter in debug builds.
///
/// This approach was described among others at: <https://nullprogram.com/blog/2023/09/27/>
///
/// # Safety
///
/// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`.
pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> {
unsafe {
#[cfg(debug_assertions)]
let conflict = conflict.map(|a| a.delegate_target_unchecked());
let index = opt_ptr_eq(conflict, Some(&S_SCRATCH[0])) as usize;
let arena = &S_SCRATCH[index];
ScratchArena::new(arena)
}
}
/// Borrows an [`Arena`] for temporary allocations.
///
/// See [`scratch_arena`].
#[cfg(debug_assertions)]
pub struct ScratchArena<'a> {
arena: debug::Arena,
offset: usize,
_phantom: std::marker::PhantomData<&'a ()>,
}
#[cfg(not(debug_assertions))]
pub struct ScratchArena<'a> {
arena: &'a Arena,
offset: usize,
}
#[cfg(debug_assertions)]
impl<'a> ScratchArena<'a> {
fn new(arena: &'a release::Arena) -> Self {
let offset = arena.offset();
ScratchArena { arena: Arena::delegated(arena), _phantom: std::marker::PhantomData, offset }
}
}
#[cfg(not(debug_assertions))]
impl<'a> ScratchArena<'a> {
fn new(arena: &'a release::Arena) -> Self {
let offset = arena.offset();
ScratchArena { arena, offset }
}
}
impl Drop for ScratchArena<'_> {
fn drop(&mut self) {
unsafe { self.arena.reset(self.offset) };
}
}
#[cfg(debug_assertions)]
impl Deref for ScratchArena<'_> {
type Target = debug::Arena;
fn deref(&self) -> &Self::Target {
&self.arena
}
}
#[cfg(not(debug_assertions))]
impl Deref for ScratchArena<'_> {
type Target = Arena;
fn deref(&self) -> &Self::Target {
self.arena
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,380 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cc"
version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "crossbeam-deque"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "grapheme-table-gen"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"indoc",
"pico-args",
"rayon",
"roxmltree",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "indoc"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
[[package]]
name = "js-sys"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "pico-args"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
[[package]]
name = "proc-macro2"
version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rayon"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
]
[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "wasm-bindgen"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
dependencies = [
"bumpalo",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@ -1,12 +0,0 @@
[package]
name = "grapheme-table-gen"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.95"
chrono = "0.4.39"
indoc = "2.0.5"
pico-args = { version = "0.5.0", features = ["eq-separator"] }
rayon = "1.10.0"
roxmltree = { version = "0.20.0", default-features = false, features = ["std"] }