added microsoft edit.

This commit is contained in:
vorboyvo 2025-05-23 12:19:11 -04:00
parent 04f9583f85
commit 58a4bbd416
75 changed files with 23069 additions and 0 deletions

View file

@ -0,0 +1,32 @@
# vvv The following parts are identical to release.toml vvv
# Avoid linking with vcruntime140.dll by statically linking everything,
# and then explicitly linking with ucrtbase.dll dynamically.
# We do this, because vcruntime140.dll is an optional Windows component.
[target.'cfg(target_os = "windows")']
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",
]
# The backtrace code for panics in Rust is almost as large as the entire editor.
# = Huge reduction in binary size by removing all that.
[unstable]
build-std = ["std", "panic_abort"]
build-std-features = ["panic_immediate_abort"]
# vvv The following parts are specific to official Windows builds. vvv
# (The use of internal registries, security features, etc., are mandatory.)
# Enable shadow stacks: https://learn.microsoft.com/en-us/cpp/build/reference/cetcompat
[target.'cfg(all(target_os = "windows", any(target_arch = "x86", target_arch = "x86_64")))']
rustflags = ["-Clink-args=/DYNAMICBASE", "-Clink-args=/CETCOMPAT"]
[registries.Edit_PublicPackages]
index = "sparse+https://pkgs.dev.azure.com/microsoft/Dart/_packaging/Edit_PublicPackages/Cargo/index/"
[source.crates-io]
replace-with = "Edit_PublicPackages"

View file

@ -0,0 +1,23 @@
# The following is not used by default via .cargo/config.toml,
# because `build-std-features` cannot be keyed by profile.
# This breaks the bench profile which doesn't support panic=abort.
# See: https://github.com/rust-lang/cargo/issues/11214
# See: https://github.com/rust-lang/cargo/issues/13894
# Avoid linking with vcruntime140.dll by statically linking everything,
# and then explicitly linking with ucrtbase.dll dynamically.
# We do this, because vcruntime140.dll is an optional Windows component.
[target.'cfg(target_os = "windows")']
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",
]
# The backtrace code for panics in Rust is almost as large as the entire editor.
# = Huge reduction in binary size by removing all that.
[unstable]
build-std = ["std", "panic_abort"]
build-std-features = ["panic_immediate_abort"]

5
pkgs/edit/.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.idea
.vs
*.profraw
lcov.info
target

View file

@ -0,0 +1,168 @@
# Documentation: https://aka.ms/obpipelines
trigger: none
parameters:
- name: debug
displayName: Enable debug output
type: boolean
default: false
- name: official
displayName: Whether to build Official or NonOfficial
type: string
default: NonOfficial
values:
- NonOfficial
- Official
- name: createvpack
displayName: Enable vpack creation
type: boolean
default: false
- name: buildPlatforms
type: object
default:
- x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc
variables:
system.debug: ${{parameters.debug}}
WindowsContainerImage: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest
# CDP_DEFINITION_BUILD_COUNT is needed for onebranch.pipeline.version task.
# See: https://aka.ms/obpipelines/versioning
CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)]
# LOAD BEARING - the vpack task fails without these
ROOT: $(Build.SourcesDirectory)
REPOROOT: $(Build.SourcesDirectory)
OUTPUTROOT: $(REPOROOT)\out
NUGET_XMLDOC_MODE: none
resources:
repositories:
- repository: GovernedTemplates
type: git
name: OneBranch.Pipelines/GovernedTemplates
ref: refs/heads/main
extends:
template: v2/Microsoft.${{parameters.official}}.yml@GovernedTemplates
parameters:
featureFlags:
WindowsHostVersion:
Version: 2022
Network: R1
platform:
name: windows_undocked
product: edit
# https://aka.ms/obpipelines/cloudvault
cloudvault:
enabled: false
# https://aka.ms/obpipelines/sdl
globalSdl:
binskim:
# > Due to some legacy reasons, 1ES PT is scanning full sources directory
# > for BinSkim tool instead of just scanning the output directory [...]
scanOutputDirectoryOnly: true
isNativeCode: true
tsa:
enabled: ${{eq(parameters.official, 'Official')}}
configFile: "$(Build.SourcesDirectory)/.pipelines/tsa.json"
stages:
# Our Build stage will build all three targets in one job, so we don't need
# to repeat most of the boilerplate work in three separate jobs.
- stage: Build
jobs:
- job: Windows
pool:
type: windows
variables:
# Binaries will go here.
# More settings at https://aka.ms/obpipelines/yaml/jobs
ob_outputDirectory: "$(Build.SourcesDirectory)/out"
# The vPack gets created from stuff in here.
# It will have a structure like:
# .../vpack/
# - amd64/
# - edit.exe
# - i386/
# - edit.exe
# - arm64/
# - edit.exe
ob_createvpack_enabled: ${{parameters.createvpack}}
ob_createvpack_vpackdirectory: "$(ob_outputDirectory)/vpack"
ob_createvpack_packagename: "windows_edit.$(Build.SourceBranchName)"
ob_createvpack_owneralias: lhecker@microsoft.com
ob_createvpack_description: Microsoft Edit
ob_createvpack_targetDestinationDirectory: "$(Destination)"
ob_createvpack_propsFile: false
ob_createvpack_provData: true
ob_createvpack_versionAs: string
ob_createvpack_version: "$(EditVersion)-$(CDP_DEFINITION_BUILD_COUNT)"
ob_createvpack_metadata: "$(Build.SourceVersion)"
ob_createvpack_topLevelRetries: 0
ob_createvpack_failOnStdErr: true
ob_createvpack_verbose: ${{ parameters.debug }}
# For details on this cargo_target_dir setting, see:
# https://eng.ms/docs/more/rust/topics/onebranch-workaround
CARGO_TARGET_DIR: C:\cargo_target_dir
# msrustup only supports stable toolchains, but this project requires nightly.
# We were told RUSTC_BOOTSTRAP=1 is a supported workaround.
RUSTC_BOOTSTRAP: 1
steps:
# NOTE: Step objects have ordered keys and you MUST have "task" as the first key.
# Objects with ordered keys... lol
- task: RustInstaller@1
displayName: Install Rust toolchain
inputs:
rustVersion: ms-stable
additionalTargets: x86_64-pc-windows-msvc aarch64-pc-windows-msvc
# URL of an Azure Artifacts feed configured with a crates.io upstream. Must be within the current ADO collection.
# NOTE: Azure Artifacts support for Rust is not yet public, but it is enabled for internal ADO organizations.
# https://learn.microsoft.com/en-us/azure/devops/artifacts/how-to/set-up-upstream-sources?view=azure-devops
cratesIoFeedOverride: sparse+https://pkgs.dev.azure.com/microsoft/Dart/_packaging/Edit_PublicPackages/Cargo/index/
# URL of an Azure Artifacts NuGet feed configured with the mscodehub Rust feed as an upstream.
# * The feed must be within the current ADO collection.
# * The CI account, usually "Project Collection Build Service (org-name)", must have at least "Collaborator" permission.
# When setting up the upstream NuGet feed, use following Azure Artifacts feed locator:
# azure-feed://mscodehub/Rust/Rust@Release
toolchainFeed: https://pkgs.dev.azure.com/microsoft/_packaging/RustTools/nuget/v3/index.json
- task: CargoAuthenticate@0
displayName: Authenticate with Azure Artifacts
inputs:
configFile: ".cargo/release-windows-ms.toml"
# We recommend making a separate `cargo fetch` step, as some build systems perform
# fetching entirely prior to the build, and perform the build with the network disabled.
- script: cargo fetch --config .cargo/release-windows-ms.toml
displayName: Fetch crates
- ${{ each platform in parameters.buildPlatforms }}:
- script: cargo build --config .cargo/release-windows-ms.toml --frozen --release --target ${{platform}}
displayName: Build ${{platform}} Release
- task: CopyFiles@2
displayName: Copy files to vpack (${{platform}})
inputs:
sourceFolder: "$(CARGO_TARGET_DIR)/${{platform}}/release"
${{ if eq(platform, 'i686-pc-windows-msvc') }}:
targetFolder: "$(ob_createvpack_vpackdirectory)/i386"
${{ elseif eq(platform, 'x86_64-pc-windows-msvc') }}:
targetFolder: "$(ob_createvpack_vpackdirectory)/amd64"
${{ else }}: # aarch64-pc-windows-msvc
targetFolder: "$(ob_createvpack_vpackdirectory)/arm64"
contents: |
*.exe
*.pdb
# Extract the version for `ob_createvpack_version`.
- script: |-
@echo off
for /f "tokens=3 delims=- " %%x in ('findstr /c:"version = " Cargo.toml') do (
echo ##vso[task.setvariable variable=EditVersion]%%~x
goto :EOF
)
displayName: "Set EditVersion"
- task: onebranch.pipeline.signing@1
displayName: "Sign files"
inputs:
command: "sign"
signing_profile: "external_distribution"
files_to_sign: "**/edit.exe"
search_root: "$(ob_createvpack_vpackdirectory)"
use_testsign: false
in_container: true

View file

@ -0,0 +1,7 @@
{
"instanceUrl": "https://microsoft.visualstudio.com",
"projectName": "OS",
"areaPath": "OS\\Windows Client and Services\\WinPD\\DFX-Developer Fundamentals and Experiences\\DEFT\\SHINE\\Commandline Tooling",
"notificationAliases": ["condev@microsoft.com", "duhowett@microsoft.com"],
"template": "VSTS_Microsoft_OSGS"
}

28
pkgs/edit/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Debug (Windows)",
"preLaunchTask": "rust: cargo build",
"type": "cppvsdbg",
"request": "launch",
"console": "externalTerminal",
"program": "${workspaceFolder}/target/debug/edit",
"cwd": "${workspaceFolder}",
"args": [
"${workspaceFolder}/src/bin/edit/main.rs"
],
},
{
"name": "Launch Debug (GDB/LLDB)",
"preLaunchTask": "rust: cargo build",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/target/debug/edit",
"cwd": "${workspaceFolder}",
"args": [
"${workspaceFolder}/src/bin/edit/main.rs"
],
}
]
}

24
pkgs/edit/.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,24 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "rust: cargo build",
"type": "process",
"command": "cargo",
"args": [
"build",
"--package",
"edit",
"--features",
"debug-latency"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$rustc"
]
}
]
}

View file

@ -0,0 +1,10 @@
# Microsoft Open Source Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
Resources:
- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/)
- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/)
- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns
- Employees can reach out at [aka.ms/opensource/moderation-support](https://aka.ms/opensource/moderation-support)

49
pkgs/edit/CONTRIBUTING.md Normal file
View file

@ -0,0 +1,49 @@
# Contributing
## Translation improvements
You can find our translations in [`src/bin/edit/localization.rs`](./src/bin/edit/localization.rs).
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.
## Bug reports
If you find any bugs, we gladly accept pull requests without prior discussion.
Otherwise, you can of course always open an issue for us to look into.
## Feature requests
Please open a new issue for any feature requests you have in mind.
Keeping the binary size of the editor small is a priority for us and so we may need to discuss any new features first until we have support for plugins.
## Code changes
The project has a focus on a small binary size and sufficient (good) performance.
As such, we generally do not accept pull requests that introduce dependencies (there are always exceptions of course).
Otherwise, you can consider this project a playground for trying out any cool ideas you have.
The overall architecture of the project can be summarized as follows:
* The underlying text buffer in `src/buffer` doesn't keep track of line breaks in the document.
This is a crucial design aspect that permeates throughout the entire codebase.
To oversimplify, the *only* state that is kept is the current cursor position.
When the user asks to move to another line, the editor will `O(n)` seek through the underlying document until it found the corresponding number of line breaks.
* As a result, `src/simd` contains crucial `memchr2` functions to quickly find the next or previous line break (runs at up to >100GB/s).
* Furthermore, `src/unicode` implements an `Utf8Chars` iterator which transparently inserts U+FFFD replacements during iteration (runs at up to 4GB/s).
* Furthermore, `src/unicode` also implements grapheme cluster segmentation and cluster width measurement via its `MeasurementConfig` (runs at up to 600MB/s).
* If word wrap is disabled, `memchr2` is used for all navigation across lines, allowing us to breeze through 1GB large files as if they were 1MB.
* Even if word-wrap is enabled, it's still sufficiently smooth thanks to `MeasurementConfig`. This is only possible because these base functions are heavily optimized.
* `src/framebuffer.rs` implements a "framebuffer" like in video games.
It allows us to draw the UI output into an intermediate buffer first, accumulating all changes and handling things like color blending.
Then, it can compare the accumulated output with the previous frame and only send the necessary changes to the terminal.
* `src/tui.rs` implements an immediate mode UI. Its module implementation gives an overview how it works and I recommend reading it.
* `src/vt.rs` implements our VT parser.
* `src/sys` contains our platform abstractions.
* Finally, `src/bin/edit` ties everything together.
It's roughly 90% UI code and business logic.
It contains a little bit of VT logic in `setup_terminal`.
If you have an issue with your terminal, the places of interest are the aforementioned:
* VT parser in `src/vt.rs`
* Platform specific code in `src/sys`
* And the `setup_terminal` function in `src/bin/edit/main.rs`

627
pkgs/edit/Cargo.lock generated Normal file
View file

@ -0,0 +1,627 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "anes"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bumpalo"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "cast"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]]
name = "clap"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
dependencies = [
"anstyle",
"clap_lex",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "criterion"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
]
[[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 = "crunchy"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "edit"
version = "1.0.0"
dependencies = [
"criterion",
"libc",
"windows-sys",
"winres",
]
[[package]]
name = "either"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "half"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]]
name = "hermit-abi"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "js-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[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.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "oorandom"
version = "11.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "plotters"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a"
[[package]]
name = "plotters-svg"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670"
dependencies = [
"plotters-backend",
]
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
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 = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
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 = "winres"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
dependencies = [
"toml",
]

53
pkgs/edit/Cargo.toml Normal file
View file

@ -0,0 +1,53 @@
[package]
name = "edit"
version = "1.0.0"
edition = "2024"
[[bench]]
name = "lib"
harness = false
[features]
debug-layout = []
debug-latency = []
# `opt-level = "s"` may be useful in the future as it significantly reduces binary size.
# We could then use the `#[optimize(speed)]` attribute for spot optimizations.
# Unfortunately, that attribute currently doesn't work on intrinsics such as memset.
[profile.release]
codegen-units = 1 # reduces binary size by ~2%
debug = "full" # No one needs an undebuggable release binary
lto = true # reduces binary size by ~14%
opt-level = "s" # reduces binary size by ~25%
panic = "abort" # reduces binary size by ~50% in combination with -Zbuild-std-features=panic_immediate_abort
split-debuginfo = "packed" # generates a seperate *.dwp/*.dSYM so the binary can get stripped
strip = "symbols" # See split-debuginfo - allows us to drop the size by ~65%
[profile.bench]
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]
winres = "0.1"
[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.5", features = ["html_reports"] }

21
pkgs/edit/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) Microsoft Corporation. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
pkgs/edit/README.md Normal file
View file

@ -0,0 +1,22 @@
# ![Application Icon for Edit](./assets/edit.svg) Edit
A simple editor for simple needs.
This editor pays homage to the classic [MS-DOS Editor](https://en.wikipedia.org/wiki/MS-DOS_Editor), but with a modern interface and input controls similar to VS Code. The goal is to provide an accessible editor that even users largely unfamiliar with terminals can easily use.
![Screenshot of Edit with the About dialog in the foreground](./assets/edit_hero_image.png)
## Installation
* Download the latest release from our [releases page](https://github.com/microsoft/edit/releases/latest)
* Extract the archive
* Copy the `edit` binary to a directory in your `PATH`
* You may delete any other files in the archive if you don't need them
## 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`

41
pkgs/edit/SECURITY.md Normal file
View file

@ -0,0 +1,41 @@
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
## Security
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
## Reporting Security Issues
**Please do not report security vulnerabilities through public GitHub issues.**
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
* Full paths of source file(s) related to the manifestation of the issue
* The location of the affected source code (tag/branch/commit or direct URL)
* Any special configuration required to reproduce the issue
* Step-by-step instructions to reproduce the issue
* Proof-of-concept or exploit code (if possible)
* Impact of the issue, including how an attacker might exploit the issue
This information will help us triage your report more quickly.
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
## Preferred Languages
We prefer all communications to be in English.
## Policy
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
<!-- END MICROSOFT SECURITY.MD BLOCK -->

View file

@ -0,0 +1,26 @@
<!-- Source: https://commons.wikimedia.org/wiki/File:Microsoft_logo_(1980).svg -->
<!-- License: Public domain -->
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 264.58333 52.916669" height="200" width="1000">
<defs id="defs2"/>
<metadata id="metadata5">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer2">
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 0,52.916667 33.602084,20.902084 V 34.925001 L 48.418751,20.902084 v 13.758334 h 8.73125 V 0.26458334 L 42.333334,15.08125 V 0.26458334 L 0,42.597917 Z" id="path847"/>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 67.468752,0.26458334 58.737501,9.2604169 V 34.660418 h 8.731251 z" id="path849"/>
<path transform="scale(0.26458334)" d="m 301.16016,1 c -21.9507,4.4255933 -39.58425,23.383151 -45.24024,48 H 255 V 53.673828 78.277344 82 h 0.69727 c 5.39479,25.07886 23.17116,44.48439 45.38085,49 H 343 v -30 h -20 v -0.004 C 322.83335,100.999 322.66667,101 322.5,101 303.44618,101 288,85.553824 288,66.5 288,47.446176 303.44618,32 322.5,32 H 342 L 372,1 Z" style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.567005;stop-color:#000000" id="path848"/>
<path transform="scale(0.26458334)" d="m 383,1 -33,34 v 96 h 33 V 33 h 18.5 c 9.66498,0 17.5,7.835017 17.5,17.5 0,9.664983 -7.83502,17.5 -17.5,17.5 H 387 L 521,199 V 157 L 487,123 443.33594,78.365234 A 47.000001,50 0 0 0 451,51 47.000001,50 0 0 0 405.00977,1.0117188 L 405,1 Z" style="display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.999999px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" id="path864"/>
<path transform="scale(0.26458334)" d="M 525.86523,1 A 68,66.499996 0 0 0 458,67.5 68,66.499996 0 0 0 526,134 68,66.499996 0 0 0 594,67.5 68,66.499996 0 0 0 526,1 68,66.499996 0 0 0 525.86523,1 Z m -1.60546,31 A 36.499998,36.000002 0 0 1 524.5,32 36.499998,36.000002 0 0 1 561,68 36.499998,36.000002 0 0 1 524.5,104 36.499998,36.000002 0 0 1 488,68 36.499998,36.000002 0 0 1 524.25977,32 Z" style="display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.567001;stop-color:#000000" id="path881"/>
<path transform="scale(0.26458334)" d="m 620.5,1 c -22.36753,-5.5e-7 -40.5,18.132467 -40.5,40.5 0,22.367533 18.13247,40.500001 40.5,40.5 h 2.5 c 11.59798,0 21,4.477153 21,10 0,5.522847 -9.40202,10 -21,10 h -40 v 29 h 62.99999 c 18.43887,-4.06734 31.56367,-20.13433 31.56446,-38.605479 C 677.56392,78.310576 669.87456,65.292316 657.38281,58.226562 640.78385,50.357003 632.38254,48.035667 620.15625,48 615.01343,46.201489 612.00162,43.42696 612,40.486328 611.9995,36.896878 616.47115,33.613784 623.55859,32 H 677 C 686.99999,16.999999 695.99998,9 709,1 h -84 z" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.567001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1" id="rect942"/>
<path transform="scale(0.26458334)" d="M 743,0 A 68.999999,68.500001 0 0 0 674,68.5 68.999999,68.500001 0 0 0 743,137 68.999999,68.500001 0 0 0 812,68.5 68.999999,68.500001 0 0 0 743,0 Z m 0.5,32 A 37.499999,36.500002 0 0 1 781,68.5 37.499999,36.500002 0 0 1 743.5,105 37.499999,36.500002 0 0 1 706,68.5 37.499999,36.500002 0 0 1 743.5,32 Z" style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.567001;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;stop-color:#000000;stop-opacity:1" id="path881-7"/>
<path id="path1086" d="m 232.03959,22.754167 v -8.73125 h -8.46667 V 8.9958336 h 9.26042 l 8.73125,-8.73125026 H 223.83751 L 214.84167,9.2604169 V 52.652085 l 8.73125,-7.672917 V 22.754167 Z" style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"/>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 251.88334,8.9958335 3.70414,-2e-7 8.7313,-8.73125014 h -20.10839 l -8.73122,8.73125034 h 7.67292 V 34.660417 l 8.7312,-7.672917 z" id="path1086-2"/>
</g>
</svg>

75
pkgs/edit/assets/edit.svg Normal file
View file

@ -0,0 +1,75 @@
<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>
</svg>

After

Width:  |  Height:  |  Size: 7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

View file

@ -0,0 +1 @@
P;1q"1;1;300;60#0;2;100;100;100#0!42?_ow{}!12?_ow{}!6?_ow{}}!5?_ow{{}}}!17~^NFbpw{}!8~!4}{wwo_!12?_oow{{{!4}!6~!4}{{wwo__!4?_ow{{}}}!23~^Nfrxw{{}}}!9~!4}{{woo_!12?_ow{}!15~^NFbpw{}!17~^NFB@-!36?_ow{}!6~!6?_ow{}!6~??w{}!7~?o{}!10~^^!10NFBpw{}!6~!8N^!9~{_!4?_o{}!8~^^!9N^^!9~{w}!8~^!18NFbx{}!9~^^!8N^^!9~}{o???ow{}!6~!11NFB@GKM!5N!10~!4NFB@-!30?_ow{}!12~_ow{}!12~??!20~FB@!15?!10~!10?r!9~???{!8~NB@!15?@FN!16~!4{!4wooo__!5?_}!8~^FB!16?@F^!8~{o!10~!9o!13?!10~-!24?_ow{}!35~??!19~x!18?!10~?CK[!4{}!9~^B??N!8~x!21?!10~N^^!18~}{o!10~!22?!29~!13?!10~-!18?_ow{}!8~^NFB@?!11~^NFB@?!10~??!10~F!9~}{wo__!12?!10~!5?@BFN^!9~}{wof^!7~}wo__!11?__o{!9~N@!7?!6@Bb!10~N!9~{o__!12?__o{}!8~F@!10~!9B!13?!10~-!12?_ow{}!8~^NFB@!7?!5~^NFB@!7?!10~??!10~??@FN^!20~??!10~!11?@BFN^!23~!7}!10~^NFB~!12}!12~^NB??BFN^!9~!10}!9~^NF@???!10~!22?!5~^NFB@-!6?_ow{}!8~^NFB@!13?FFB@!13?!10F??!10F!7?@@BB!15F??!10F!17?@BFN^!10~|zrfFF!10NFFFBB@@!5?!21FBB@!11?@BBFFNNN!10^NNNFFBB@!8?!10~!22?NFB@-_ow{}!8~^NFB@!119?@BFN^!9~}{wo!88?!10~-!7~^NFB@!131?@BFN^!7~!88?!7~^NF-~^NFB@!143?@BFN^~!88?~^NFB@\

116
pkgs/edit/benches/lib.rs Normal file
View file

@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::hint::black_box;
use std::mem;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use edit::helpers::*;
use edit::simd::MemsetSafe;
use edit::{hash, oklab, simd, unicode};
fn bench_hash(c: &mut Criterion) {
c.benchmark_group("hash")
.throughput(Throughput::Bytes(8))
.bench_function(BenchmarkId::new("hash", 8), |b| {
let data = [0u8; 8];
b.iter(|| hash::hash(0, black_box(&data)))
})
.throughput(Throughput::Bytes(16))
.bench_function(BenchmarkId::new("hash", 16), |b| {
let data = [0u8; 16];
b.iter(|| hash::hash(0, black_box(&data)))
})
.throughput(Throughput::Bytes(1024))
.bench_function(BenchmarkId::new("hash", 1024), |b| {
let data = [0u8; 1024];
b.iter(|| hash::hash(0, black_box(&data)))
});
}
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)))
});
}
fn bench_simd_memchr2(c: &mut Criterion) {
let mut group = c.benchmark_group("simd");
let mut buffer_u8 = [0u8; 2048];
for &bytes in &[8usize, 32 + 8, 64 + 8, KIBI + 8] {
group.throughput(Throughput::Bytes(bytes as u64 + 1)).bench_with_input(
BenchmarkId::new("memchr2", bytes),
&bytes,
|b, &size| {
buffer_u8.fill(b'a');
buffer_u8[size] = b'\n';
b.iter(|| simd::memchr2(b'\n', b'\r', black_box(&buffer_u8), 0));
},
);
}
}
fn bench_simd_memset<T: MemsetSafe + Copy + Default>(c: &mut Criterion) {
let mut group = c.benchmark_group("simd");
let name = format!("memset<{}>", std::any::type_name::<T>());
let size = mem::size_of::<T>();
let mut buf: Vec<T> = vec![Default::default(); 2048 / size];
for &bytes in &[8usize, 32 + 8, 64 + 8, KIBI + 8] {
group.throughput(Throughput::Bytes(bytes as u64)).bench_with_input(
BenchmarkId::new(&name, bytes),
&bytes,
|b, &bytes| {
let slice = unsafe { buf.get_unchecked_mut(..bytes / size) };
b.iter(|| simd::memset(black_box(slice), Default::default()));
},
);
}
}
fn bench_unicode(c: &mut Criterion) {
let reference = concat!(
"In the quiet twilight, dreams unfold, soft whispers of a story untold.\n",
"月明かりが静かに照らし出し、夢を見る心の奥で詩が静かに囁かれる\n",
"Stars collide in the early light of hope, echoing the silent call of the night.\n",
"夜の静寂、希望と孤独が混ざり合うその中で詩が永遠に続く\n",
);
let buffer = reference.repeat(10);
let bytes = buffer.as_bytes();
c.benchmark_group("unicode::MeasurementConfig::goto_logical")
.throughput(Throughput::Bytes(bytes.len() as u64))
.bench_function("basic", |b| {
b.iter(|| unicode::MeasurementConfig::new(&bytes).goto_logical(Point::MAX))
})
.bench_function("word_wrap", |b| {
b.iter(|| {
unicode::MeasurementConfig::new(black_box(&bytes))
.with_word_wrap_column(50)
.goto_logical(Point::MAX)
})
});
c.benchmark_group("unicode::Utf8Chars")
.throughput(Throughput::Bytes(bytes.len() as u64))
.bench_function("next", |b| {
b.iter(|| {
unicode::Utf8Chars::new(bytes, 0).fold(0u32, |acc, ch| acc.wrapping_add(ch as u32))
})
});
}
fn bench(c: &mut Criterion) {
bench_hash(c);
bench_oklab(c);
bench_simd_memchr2(c);
bench_simd_memset::<u32>(c);
bench_simd_memset::<u8>(c);
bench_unicode(c);
}
criterion_group!(benches, bench);
criterion_main!(benches);

14
pkgs/edit/build.rs Normal file
View file

@ -0,0 +1,14 @@
// 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" {
winres::WindowsResource::new()
.set_manifest_file("src/bin/edit/edit.exe.manifest")
.set("FileDescription", "Microsoft Edit")
.set("LegalCopyright", "© Microsoft Corporation. All rights reserved.")
.compile()
.unwrap();
}
}

27
pkgs/edit/edit.nix Normal file
View file

@ -0,0 +1,27 @@
{
lib,
pkgs
}:
let
rustPlatform = pkgs.makeRustPlatform {
cargo = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default);
rustc = pkgs.rust-bin.selectLatestNightlyWith (toolchain: toolchain.default);
};
in
rustPlatform.buildRustPackage (finalAttrs: {
pname = "edit";
version = "1.0.0";
src = ./.;
cargoHash = "sha256-DEzjfrXSmum/GJdYanaRDKxG4+eNPWf5echLhStxcIg=";
meta = {
description = "We all edit.";
homepage = "https://github.com/microsoft/edit";
license = lib.licenses.mit;
maintainers = [ ];
};
})

99
pkgs/edit/flake.lock Normal file
View file

@ -0,0 +1,99 @@
{
"nodes": {
"fenix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1747392669,
"narHash": "sha256-zky3+lndxKRu98PAwVK8kXPdg+Q1NVAhaI7YGrboKYA=",
"owner": "nix-community",
"repo": "fenix",
"rev": "c3c27e603b0d9b5aac8a16236586696338856fbb",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1747744144,
"narHash": "sha256-W7lqHp0qZiENCDwUZ5EX/lNhxjMdNapFnbErcbnP11Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2795c506fe8fb7b03c36ccb51f75b6df0ab2553f",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1747323949,
"narHash": "sha256-G4NwzhODScKnXqt2mEQtDFOnI0wU3L1WxsiHX3cID/0=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "f8e784353bde7cbf9a9046285c1caf41ac484ebe",
"type": "github"
},
"original": {
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

39
pkgs/edit/flake.nix Normal file
View file

@ -0,0 +1,39 @@
{
description = "Flake containing Edit";
inputs = {
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
nixpkgs.url = "nixpkgs/nixos-unstable";
};
outputs = { self, fenix, flake-utils, nixpkgs }:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = nixpkgs.legacyPackages.${system}; in
{
defaultPackage =
let rustPlatform = (pkgs.makeRustPlatform {
inherit (fenix.packages.${system}.minimal) cargo rustc;
});
in rustPlatform.buildRustPackage {
pname = "edit";
version = "1.0.0";
src = ./.;
cargoHash = "sha256-DEzjfrXSmum/GJdYanaRDKxG4+eNPWf5echLhStxcIg=";
meta = {
description = "We all edit.";
homepage = "https://github.com/microsoft/edit";
license = pkgs.lib.licenses.mit;
maintainers = [ ];
};
};
}
);
}

62
pkgs/edit/hi.nix Normal file
View file

@ -0,0 +1,62 @@
{
description = "core-lending";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"aarch64-darwin"
];
perSystem = { self', pkgs, system, ... }:
let
rustVersion = "1.86.0";
rust = pkgs.rust-bin.stable.${rustVersion};
rustPlatform = pkgs.makeRustPlatform {
rustc = rust.minimal;
cargo = rust.minimal;
};
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
in
{
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ inputs.rust-overlay.overlays.default ];
};
packages.default = rustPlatform.buildRustPackage {
pname = cargoToml.package.name;
version = cargoToml.package.version;
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
};
};
devShells.default = pkgs.mkShell {
buildInputs = [
pkgs.diesel-cli
pkgs.elmPackages.elm
pkgs.elmPackages.elm-language-server
pkgs.postgresql_17
(rust.default.override {
extensions = [ "rust-analyzer" "rust-src" ];
})
];
shellHook = ''
export LANG="en_US.UTF-8"
'';
};
};
};
}

1
pkgs/edit/result Symbolic link
View file

@ -0,0 +1 @@
/nix/store/inr2b18j80dc17bkaz5xm7f20g0i3s0r-edit-1.0.0

7
pkgs/edit/rustfmt.toml Normal file
View file

@ -0,0 +1,7 @@
style_edition = "2024"
use_small_heuristics = "Max"
group_imports = "StdExternalCrate"
imports_granularity = "Module"
format_code_in_doc_comments = true
newline_style = "Unix"
use_field_init_shorthand = true

42
pkgs/edit/src/apperr.rs Normal file
View file

@ -0,0 +1,42 @@
// 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 {
Error::App(code)
}
pub const fn new_icu(code: u32) -> Self {
Error::Icu(code)
}
pub const fn new_sys(code: u32) -> Self {
Error::Sys(code)
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
sys::io_error_to_apperr(err)
}
}

View file

@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(clippy::missing_safety_doc, clippy::mut_from_ref)]
use std::alloc::{AllocError, Allocator, Layout};
use std::mem::{self, MaybeUninit};
use std::ptr::NonNull;
use super::release;
use crate::apperr;
/// A debug wrapper for [`release::Arena`].
///
/// The problem with [`super::ScratchArena`] is that it only "borrows" an underlying
/// [`release::Arena`]. Once the [`super::ScratchArena`] is dropped it resets the watermark
/// of the underlying [`release::Arena`], freeing all allocations done since borrowing it.
///
/// 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;
///
/// let mut scratch1 = scratch_arena(None);
/// let mut scratch2 = scratch_arena(None);
///
/// let foo = scratch1.alloc_uninit::<usize>();
///
/// // This will also reset `scratch1`'s allocation.
/// drop(scratch2);
///
/// *foo; // BOOM! ...if it wasn't for our debug wrapper.
/// ```
///
/// To avoid this, this wraps the real [`release::Arena`] in a "debug" one, which pretends as if every
/// instance of itself is a distinct [`release::Arena`] instance. Then we use this "debug" [`release::Arena`]
/// for [`super::ScratchArena`] which allows us to track which borrow is the most recent one.
pub enum Arena {
// Delegate is 'static, because release::Arena requires no lifetime
// annotations, and so this mere debug helper cannot use them either.
Delegated { delegate: &'static release::Arena, borrow: usize },
Owned { arena: release::Arena },
}
impl Drop for Arena {
fn drop(&mut self) {
if let Arena::Delegated { delegate, borrow } = self {
let borrows = delegate.borrows.get();
assert_eq!(*borrow, borrows);
delegate.borrows.set(borrows - 1);
}
}
}
impl Default for Arena {
fn default() -> Self {
Self::empty()
}
}
impl Arena {
pub const fn empty() -> Self {
Self::Owned { arena: release::Arena::empty() }
}
pub fn new(capacity: usize) -> apperr::Result<Arena> {
Ok(Self::Owned { arena: release::Arena::new(capacity)? })
}
pub(super) fn delegated(delegate: &release::Arena) -> Arena {
let borrow = delegate.borrows.get() + 1;
delegate.borrows.set(borrow);
Self::Delegated { delegate: unsafe { mem::transmute(delegate) }, borrow }
}
#[inline]
pub(super) fn delegate_target(&self) -> &release::Arena {
match *self {
Arena::Delegated { delegate, borrow } => {
assert!(
borrow == delegate.borrows.get(),
"Arena already borrowed by a newer ScratchArena"
);
delegate
}
Arena::Owned { ref arena } => arena,
}
}
#[inline]
pub(super) fn delegate_target_unchecked(&self) -> &release::Arena {
match self {
Arena::Delegated { delegate, .. } => delegate,
Arena::Owned { arena } => arena,
}
}
pub fn offset(&self) -> usize {
self.delegate_target().offset()
}
pub unsafe fn reset(&self, to: usize) {
unsafe { self.delegate_target().reset(to) }
}
pub fn alloc_uninit<T>(&self) -> &mut MaybeUninit<T> {
self.delegate_target().alloc_uninit()
}
pub fn alloc_uninit_slice<T>(&self, count: usize) -> &mut [MaybeUninit<T>] {
self.delegate_target().alloc_uninit_slice(count)
}
}
unsafe impl Allocator for Arena {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
self.delegate_target().alloc_raw(layout.size(), layout.align())
}
fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
self.delegate_target().allocate_zeroed(layout)
}
// While it is possible to shrink the tail end of the arena, it is
// not very useful given the existence of scoped scratch arenas.
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
unsafe { self.delegate_target().deallocate(ptr, layout) }
}
unsafe fn grow(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
unsafe { self.delegate_target().grow(ptr, old_layout, new_layout) }
}
unsafe fn grow_zeroed(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
unsafe { self.delegate_target().grow_zeroed(ptr, old_layout, new_layout) }
}
unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
unsafe { self.delegate_target().shrink(ptr, old_layout, new_layout) }
}
}

View file

@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Arena allocators. Small and fast.
#[cfg(debug_assertions)]
mod debug;
mod release;
mod scratch;
mod string;
#[cfg(all(not(doc), debug_assertions))]
pub use self::debug::Arena;
#[cfg(any(doc, not(debug_assertions)))]
pub use self::release::Arena;
pub use self::scratch::{ScratchArena, init, scratch_arena};
pub use self::string::ArenaString;

View file

@ -0,0 +1,278 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![allow(clippy::mut_from_ref)]
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 crate::helpers::*;
use crate::{apperr, sys};
const ALLOC_CHUNK_SIZE: usize = 64 * KIBI;
/// An arena allocator.
///
/// If you have never used an arena allocator before, think of it as
/// allocating objects on the stack, but the stack is *really* big.
/// Each time you allocate, memory gets pushed at the end of the stack,
/// each time you deallocate, memory gets popped from the end of the stack.
///
/// One reason you'd want to use this is obviously performance: It's very simple
/// and so it's also very fast, >10x faster than your system allocator.
///
/// However, modern allocators such as `mimalloc` are just as fast, so why not use them?
/// Because their performance comes at the cost of binary size and we can't have that.
///
/// The biggest benefit though is that it sometimes massively simplifies lifetime
/// and memory management. This can best be seen by this project's UI code, which
/// uses an arena to allocate a tree of UI nodes. This is infameously difficult
/// to do in Rust, but not so when you got an arena allocator:
/// All nodes have the same lifetime, so you can just use references.
///
/// # Safety
///
/// **Do not** push objects into the arena that require destructors.
/// Destructors are not executed. Use a pool allocator for that.
pub struct Arena {
base: NonNull<u8>,
capacity: usize,
commit: Cell<usize>,
offset: Cell<usize>,
/// See [`super::debug`], which uses this for borrow tracking.
#[cfg(debug_assertions)]
pub(super) borrows: Cell<usize>,
}
impl Arena {
pub const fn empty() -> Self {
Self {
base: NonNull::dangling(),
capacity: 0,
commit: Cell::new(0),
offset: Cell::new(0),
#[cfg(debug_assertions)]
borrows: Cell::new(0),
}
}
pub fn new(capacity: usize) -> apperr::Result<Arena> {
let capacity = (capacity.max(1) + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
let base = unsafe { sys::virtual_reserve(capacity)? };
Ok(Arena {
base,
capacity,
commit: Cell::new(0),
offset: Cell::new(0),
#[cfg(debug_assertions)]
borrows: Cell::new(0),
})
}
pub fn offset(&self) -> usize {
self.offset.get()
}
/// "Deallocates" the memory in the arena down to the given offset.
///
/// # Safety
///
/// Obviously, this is GIGA UNSAFE. It runs no destructors and does not check
/// whether the offset is valid. You better take care when using this function.
pub unsafe fn reset(&self, to: usize) {
// Fill the deallocated memory with 0xDD to aid debugging.
if cfg!(debug_assertions) && self.offset.get() > to {
let commit = self.commit.get();
let len = (self.offset.get() + 128).min(commit) - to;
unsafe { slice::from_raw_parts_mut(self.base.add(to).as_ptr(), len).fill(0xDD) };
}
self.offset.replace(to);
}
#[inline]
pub(super) fn alloc_raw(
&self,
bytes: usize,
alignment: usize,
) -> Result<NonNull<[u8]>, AllocError> {
let commit = self.commit.get();
let offset = self.offset.get();
let beg = (offset + alignment - 1) & !(alignment - 1);
let end = beg + bytes;
if end > commit {
return self.alloc_raw_bump(beg, end);
}
if cfg!(debug_assertions) {
let ptr = unsafe { self.base.add(offset) };
let len = (end + 128).min(self.commit.get()) - offset;
unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), len).fill(0xCD) };
}
self.offset.replace(end);
Ok(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> {
let offset = self.offset.get();
let commit_old = self.commit.get();
let commit_new = (end + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
if commit_new > self.capacity
|| unsafe {
sys::virtual_commit(self.base.add(commit_old), commit_new - commit_old).is_err()
}
{
return Err(AllocError);
}
if cfg!(debug_assertions) {
let ptr = unsafe { self.base.add(offset) };
let len = (end + 128).min(self.commit.get()) - offset;
unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), len).fill(0xCD) };
}
self.commit.replace(commit_new);
self.offset.replace(end);
Ok(unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), end - beg) })
}
#[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();
unsafe { ptr.cast().as_mut() }
}
#[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();
unsafe { slice::from_raw_parts_mut(ptr.cast().as_ptr(), count) }
}
}
impl Drop for Arena {
fn drop(&mut self) {
if self.base != NonNull::dangling() {
unsafe { sys::virtual_release(self.base, self.capacity) };
}
}
}
impl Default for Arena {
fn default() -> Self {
Arena::empty()
}
}
unsafe impl Allocator for Arena {
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
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())?;
unsafe { p.cast::<u8>().as_ptr().write_bytes(0, p.len()) }
Ok(p)
}
// While it is possible to shrink the tail end of the arena, it is
// not very useful given the existence of scoped scratch arenas.
unsafe fn deallocate(&self, _: NonNull<u8>, _: Layout) {}
unsafe fn grow(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
debug_assert!(new_layout.size() >= old_layout.size());
debug_assert!(new_layout.align() <= old_layout.align());
let new_ptr;
// Growing the given area is possible if it is at the end of the arena.
if unsafe { ptr.add(old_layout.size()) == self.base.add(self.offset.get()) } {
new_ptr = ptr;
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)?;
} else {
cold_path();
new_ptr = self.allocate(new_layout)?.cast();
// SAFETY: It's weird to me that this doesn't assert new_layout.size() >= old_layout.size(),
// but neither does the stdlib code at the time of writing.
// So, assuming that is not needed, this code is safe since it just copies the old data over.
unsafe {
ptr::copy_nonoverlapping(ptr.as_ptr(), new_ptr.as_ptr(), old_layout.size());
self.deallocate(ptr, old_layout);
}
}
Ok(NonNull::slice_from_raw_parts(new_ptr, new_layout.size()))
}
unsafe fn grow_zeroed(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
unsafe {
// SAFETY: Same as grow().
let ptr = self.grow(ptr, old_layout, new_layout)?;
// SAFETY: At this point, `ptr` must be valid for `new_layout.size()` bytes,
// allowing us to safely zero out the delta since `old_layout.size()`.
ptr.cast::<u8>()
.add(old_layout.size())
.write_bytes(0, new_layout.size() - old_layout.size());
Ok(ptr)
}
}
unsafe fn shrink(
&self,
ptr: NonNull<u8>,
old_layout: Layout,
new_layout: Layout,
) -> Result<NonNull<[u8]>, AllocError> {
debug_assert!(new_layout.size() <= old_layout.size());
debug_assert!(new_layout.align() <= old_layout.align());
let mut len = old_layout.size();
// Shrinking the given area is possible if it is at the end of the arena.
if unsafe { ptr.add(len) == self.base.add(self.offset.get()) } {
self.offset.set(self.offset.get() - len + new_layout.size());
len = new_layout.size();
} else {
debug_assert!(
false,
"Did you call shrink_to_fit()? Only the last allocation can be shrunk!"
);
}
Ok(NonNull::slice_from_raw_parts(ptr, len))
}
}

View file

@ -0,0 +1,112 @@
// 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 = &mut 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
}
}

View file

@ -0,0 +1,281 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::fmt;
use std::ops::{Bound, Deref, DerefMut, RangeBounds};
use super::Arena;
use crate::helpers::*;
/// A custom string type, because `std` lacks allocator support for [`String`].
///
/// To keep things simple, this one is hardcoded to [`Arena`].
#[derive(Clone)]
pub struct ArenaString<'a> {
vec: Vec<u8, &'a Arena>,
}
impl<'a> ArenaString<'a> {
/// Creates a new [`ArenaString`] in the given arena.
#[must_use]
pub const fn new_in(arena: &'a Arena) -> Self {
Self { vec: Vec::new_in(arena) }
}
#[must_use]
pub fn with_capacity_in(capacity: usize, arena: &'a Arena) -> Self {
Self { vec: Vec::with_capacity_in(capacity, arena) }
}
/// Turns a [`str`] into an [`ArenaString`].
#[must_use]
pub fn from_str(arena: &'a Arena, s: &str) -> Self {
let mut res = Self::new_in(arena);
res.push_str(s);
res
}
/// It says right here that you checked if `bytes` is valid UTF-8
/// and you are sure it is. Presto! Here's an `ArenaString`!
///
/// # Safety
///
/// You fool! It says "unchecked" right there. Now the house is burning.
#[inline]
#[must_use]
pub unsafe fn from_utf8_unchecked(bytes: Vec<u8, &'a Arena>) -> Self {
Self { vec: bytes }
}
/// Checks whether `text` contains only valid UTF-8.
/// If the entire string is valid, it returns `Ok(text)`.
/// Otherwise, it returns `Err(ArenaString)` with all invalid sequences replaced with U+FFFD.
pub fn from_utf8_lossy<'s>(
arena: &'a Arena,
text: &'s [u8],
) -> Result<&'s str, ArenaString<'a>> {
let mut iter = text.utf8_chunks();
let Some(mut chunk) = iter.next() else {
return Ok("");
};
let valid = chunk.valid();
if chunk.invalid().is_empty() {
debug_assert_eq!(valid.len(), text.len());
return Ok(unsafe { str::from_utf8_unchecked(text) });
}
const REPLACEMENT: &str = "\u{FFFD}";
let mut res = Self::new_in(arena);
res.reserve(text.len());
loop {
res.push_str(chunk.valid());
if !chunk.invalid().is_empty() {
res.push_str(REPLACEMENT);
}
chunk = match iter.next() {
Some(chunk) => chunk,
None => break,
};
}
Err(res)
}
/// Turns a [`Vec<u8>`] into an [`ArenaString`], replacing invalid UTF-8 sequences with U+FFFD.
#[must_use]
pub fn from_utf8_lossy_owned(v: Vec<u8, &'a Arena>) -> Self {
match Self::from_utf8_lossy(v.allocator(), &v) {
Ok(..) => unsafe { Self::from_utf8_unchecked(v) },
Err(s) => s,
}
}
/// It's empty.
pub fn is_empty(&self) -> bool {
self.vec.is_empty()
}
/// It's lengthy.
pub fn len(&self) -> usize {
self.vec.len()
}
/// It's capacatity.
pub fn capacity(&self) -> usize {
self.vec.capacity()
}
/// It's a [`String`], now it's a [`str`]. Wow!
pub fn as_str(&self) -> &str {
unsafe { str::from_utf8_unchecked(self.vec.as_slice()) }
}
/// It's a [`String`], now it's a [`str`]. And it's mutable! WOW!
pub fn as_mut_str(&mut self) -> &mut str {
unsafe { str::from_utf8_unchecked_mut(self.vec.as_mut_slice()) }
}
/// Now it's bytes!
pub fn as_bytes(&self) -> &[u8] {
self.vec.as_slice()
}
/// Returns a mutable reference to the contents of this `String`.
///
/// # Safety
///
/// The underlying `&mut Vec` allows writing bytes which are not valid UTF-8.
pub unsafe fn as_mut_vec(&mut self) -> &mut Vec<u8, &'a Arena> {
&mut self.vec
}
/// Reserves *additional* memory. For you old folks out there (totally not me),
/// this is differrent from C++'s `reserve` which reserves a total size.
pub fn reserve(&mut self, additional: usize) {
self.vec.reserve(additional)
}
/// Just like [`ArenaString::reserve`], but it doesn't overallocate.
pub fn reserve_exact(&mut self, additional: usize) {
self.vec.reserve_exact(additional)
}
/// Now it's small! Alarming!
///
/// *Do not* call this unless this string is the last thing on the arena.
/// Arenas are stacks, they can't deallocate what's in the middle.
pub fn shrink_to_fit(&mut self) {
self.vec.shrink_to_fit()
}
/// To no surprise, this clears the string.
pub fn clear(&mut self) {
self.vec.clear()
}
/// Append some text.
pub fn push_str(&mut self, string: &str) {
self.vec.extend_from_slice(string.as_bytes())
}
/// Append a single character.
#[inline]
pub fn push(&mut self, ch: char) {
match ch.len_utf8() {
1 => self.vec.push(ch as u8),
_ => self.vec.extend_from_slice(ch.encode_utf8(&mut [0; 4]).as_bytes()),
}
}
/// Same as `push(char)` but with a specified number of character copies.
/// Shockingly absent from the standard library.
pub fn push_repeat(&mut self, ch: char, total_copies: usize) {
if total_copies == 0 {
return;
}
let buf = unsafe { self.as_mut_vec() };
if ch.is_ascii() {
// Compiles down to `memset()`.
buf.extend(std::iter::repeat_n(ch as u8, total_copies));
} else {
// Implements efficient string padding using quadratic duplication.
let mut utf8_buf = [0; 4];
let utf8 = ch.encode_utf8(&mut utf8_buf).as_bytes();
let initial_len = buf.len();
let added_len = utf8.len() * total_copies;
let final_len = initial_len + added_len;
buf.reserve(added_len);
buf.extend_from_slice(utf8);
while buf.len() != final_len {
let end = (final_len - buf.len() + initial_len).min(buf.len());
buf.extend_from_within(initial_len..end);
}
}
}
/// Replaces a range of characters with a new string.
pub fn replace_range<R: RangeBounds<usize>>(&mut self, range: R, replace_with: &str) {
match range.start_bound() {
Bound::Included(&n) => assert!(self.is_char_boundary(n)),
Bound::Excluded(&n) => assert!(self.is_char_boundary(n + 1)),
Bound::Unbounded => {}
};
match range.end_bound() {
Bound::Included(&n) => assert!(self.is_char_boundary(n + 1)),
Bound::Excluded(&n) => assert!(self.is_char_boundary(n)),
Bound::Unbounded => {}
};
unsafe { self.as_mut_vec() }.replace_range(range, replace_with.as_bytes());
}
/// Finds `old` in the string and replaces it with `new`.
/// Only performs one replacement.
pub fn replace_once_in_place(&mut self, old: &str, new: &str) {
if let Some(beg) = self.find(old) {
unsafe { self.as_mut_vec() }.replace_range(beg..beg + old.len(), new.as_bytes());
}
}
}
impl fmt::Debug for ArenaString<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&**self, f)
}
}
impl PartialEq<&str> for ArenaString<'_> {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl Deref for ArenaString<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl DerefMut for ArenaString<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.as_mut_str()
}
}
impl fmt::Display for ArenaString<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl fmt::Write for ArenaString<'_> {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.push_str(s);
Ok(())
}
#[inline]
fn write_char(&mut self, c: char) -> fmt::Result {
self.push(c);
Ok(())
}
}
#[macro_export]
macro_rules! arena_format {
($arena:expr, $($arg:tt)*) => {{
use std::fmt::Write as _;
let mut output = $crate::arena::ArenaString::new_in($arena);
output.write_fmt(format_args!($($arg)*)).unwrap();
output
}}
}

121
pkgs/edit/src/base64.rs Normal file
View file

@ -0,0 +1,121 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Base64 facilities.
use crate::arena::ArenaString;
const CHARSET: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
/// One aspect of base64 is that the encoded length can be
/// calculated accurately in advance, which is what this returns.
#[inline]
pub fn encode_len(src_len: usize) -> usize {
src_len.div_ceil(3) * 4
}
/// Encodes the given bytes as base64 and appends them to the destination string.
pub fn encode(dst: &mut ArenaString, src: &[u8]) {
unsafe {
let mut inp = src.as_ptr();
let mut remaining = src.len();
let dst = dst.as_mut_vec();
let out_len = encode_len(src.len());
// ... we can then use this fact to reserve space all at once.
dst.reserve(out_len);
// SAFETY: Getting a pointer to the reserved space is only safe
// *after* calling `reserve()` as it may change the pointer.
let mut out = dst.as_mut_ptr().add(dst.len());
if remaining != 0 {
// Translate chunks of 3 source bytes into 4 base64-encoded bytes.
while remaining > 3 {
// SAFETY: Thanks to `remaining > 3`, reading 4 bytes at once is safe.
// This improves performance massively over a byte-by-byte approach,
// because it allows us to byte-swap the read and use simple bit-shifts below.
let val = u32::from_be((inp as *const u32).read_unaligned());
inp = inp.add(3);
remaining -= 3;
*out = CHARSET[(val >> 26) as usize];
out = out.add(1);
*out = CHARSET[(val >> 20) as usize & 0x3f];
out = out.add(1);
*out = CHARSET[(val >> 14) as usize & 0x3f];
out = out.add(1);
*out = CHARSET[(val >> 8) as usize & 0x3f];
out = out.add(1);
}
// Convert the remaining 1-3 bytes.
let mut in1 = 0;
let mut in2 = 0;
// We can simplify the following logic by assuming that there's only 1
// byte left. If there's >1 byte left, these two '=' will be overwritten.
*out.add(3) = b'=';
*out.add(2) = b'=';
if remaining >= 3 {
in2 = inp.add(2).read() as usize;
*out.add(3) = CHARSET[in2 & 0x3f];
}
if remaining >= 2 {
in1 = inp.add(1).read() as usize;
*out.add(2) = CHARSET[(in1 << 2 | in2 >> 6) & 0x3f];
}
let in0 = inp.add(0).read() as usize;
*out.add(1) = CHARSET[(in0 << 4 | in1 >> 4) & 0x3f];
*out.add(0) = CHARSET[in0 >> 2];
}
dst.set_len(dst.len() + out_len);
}
}
#[cfg(test)]
mod tests {
use super::encode;
use crate::arena::{Arena, ArenaString};
#[test]
fn test_basic() {
let arena = Arena::new(4 * 1024).unwrap();
let enc = |s: &[u8]| {
let mut dst = ArenaString::new_in(&arena);
encode(&mut dst, s);
dst
};
assert_eq!(enc(b""), "");
assert_eq!(enc(b"a"), "YQ==");
assert_eq!(enc(b"ab"), "YWI=");
assert_eq!(enc(b"abc"), "YWJj");
assert_eq!(enc(b"abcd"), "YWJjZA==");
assert_eq!(enc(b"abcde"), "YWJjZGU=");
assert_eq!(enc(b"abcdef"), "YWJjZGVm");
assert_eq!(enc(b"abcdefg"), "YWJjZGVmZw==");
assert_eq!(enc(b"abcdefgh"), "YWJjZGVmZ2g=");
assert_eq!(enc(b"abcdefghi"), "YWJjZGVmZ2hp");
assert_eq!(enc(b"abcdefghij"), "YWJjZGVmZ2hpag==");
assert_eq!(enc(b"abcdefghijk"), "YWJjZGVmZ2hpams=");
assert_eq!(enc(b"abcdefghijkl"), "YWJjZGVmZ2hpamts");
assert_eq!(enc(b"abcdefghijklm"), "YWJjZGVmZ2hpamtsbQ==");
assert_eq!(enc(b"abcdefghijklmN"), "YWJjZGVmZ2hpamtsbU4=");
assert_eq!(enc(b"abcdefghijklmNO"), "YWJjZGVmZ2hpamtsbU5P");
assert_eq!(enc(b"abcdefghijklmNOP"), "YWJjZGVmZ2hpamtsbU5PUA==");
assert_eq!(enc(b"abcdefghijklmNOPQ"), "YWJjZGVmZ2hpamtsbU5PUFE=");
assert_eq!(enc(b"abcdefghijklmNOPQR"), "YWJjZGVmZ2hpamtsbU5PUFFS");
assert_eq!(enc(b"abcdefghijklmNOPQRS"), "YWJjZGVmZ2hpamtsbU5PUFFSUw==");
assert_eq!(enc(b"abcdefghijklmNOPQRST"), "YWJjZGVmZ2hpamtsbU5PUFFSU1Q=");
assert_eq!(enc(b"abcdefghijklmNOPQRSTU"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RV");
assert_eq!(enc(b"abcdefghijklmNOPQRSTUV"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVg==");
assert_eq!(enc(b"abcdefghijklmNOPQRSTUVW"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVlc=");
assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWX"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldY");
assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXY"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWQ==");
assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXYZ"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWVo=");
}
}

View file

@ -0,0 +1,296 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::collections::LinkedList;
use std::ffi::OsStr;
use std::fs::File;
use std::path::{Path, PathBuf};
use edit::buffer::{RcTextBuffer, TextBuffer};
use edit::helpers::{CoordType, Point};
use edit::simd::memrchr2;
use edit::{apperr, path, sys};
use crate::state::DisplayablePathBuf;
pub struct Document {
pub buffer: RcTextBuffer,
pub path: Option<PathBuf>,
pub dir: Option<DisplayablePathBuf>,
pub filename: String,
pub file_id: Option<sys::FileId>,
pub new_file_counter: usize,
}
impl Document {
pub fn save(&mut self, new_path: Option<PathBuf>) -> apperr::Result<()> {
let path = new_path.as_deref().unwrap_or_else(|| self.path.as_ref().unwrap().as_path());
let mut file = DocumentManager::open_for_writing(path)?;
{
let mut tb = self.buffer.borrow_mut();
tb.write_file(&mut file)?;
}
if let Ok(id) = sys::file_id(&file) {
self.file_id = Some(id);
}
if let Some(path) = new_path {
self.set_path(path);
}
Ok(())
}
pub fn reread(&mut self, encoding: Option<&'static str>) -> apperr::Result<()> {
let path = self.path.as_ref().unwrap().as_path();
let mut file = DocumentManager::open_for_reading(path)?;
{
let mut tb = self.buffer.borrow_mut();
tb.read_file(&mut file, encoding)?;
}
if let Ok(id) = sys::file_id(&file) {
self.file_id = Some(id);
}
Ok(())
}
fn set_path(&mut self, path: PathBuf) {
let filename = path.file_name().unwrap_or_default().to_string_lossy().into_owned();
let dir = path.parent().map(ToOwned::to_owned).unwrap_or_default();
self.filename = filename;
self.dir = Some(DisplayablePathBuf::new(dir));
self.path = Some(path);
self.update_file_mode();
}
fn update_file_mode(&mut self) {
let mut tb = self.buffer.borrow_mut();
tb.set_ruler(if self.filename == "COMMIT_EDITMSG" { 72 } else { 0 });
}
}
#[derive(Default)]
pub struct DocumentManager {
list: LinkedList<Document>,
}
impl DocumentManager {
#[inline]
pub fn len(&self) -> usize {
self.list.len()
}
#[inline]
pub fn active(&self) -> Option<&Document> {
self.list.front()
}
#[inline]
pub fn active_mut(&mut self) -> Option<&mut Document> {
self.list.front_mut()
}
#[inline]
pub fn update_active<F: FnMut(&Document) -> bool>(&mut self, mut func: F) -> bool {
let mut cursor = self.list.cursor_front_mut();
while let Some(doc) = cursor.current() {
if func(doc) {
let list = cursor.remove_current_as_list().unwrap();
self.list.cursor_front_mut().splice_before(list);
return true;
}
cursor.move_next();
}
false
}
pub fn remove_active(&mut self) {
self.list.pop_front();
}
pub fn add_untitled(&mut self) -> apperr::Result<&mut Document> {
let buffer = TextBuffer::new_rc(false)?;
{
let mut tb = buffer.borrow_mut();
tb.set_margin_enabled(true);
tb.set_line_highlight_enabled(true);
}
let mut doc = Document {
buffer,
path: None,
dir: Default::default(),
filename: Default::default(),
file_id: None,
new_file_counter: 0,
};
self.gen_untitled_name(&mut doc);
self.list.push_front(doc);
Ok(self.list.front_mut().unwrap())
}
pub fn gen_untitled_name(&self, doc: &mut Document) {
let mut new_file_counter = 0;
for doc in &self.list {
new_file_counter = new_file_counter.max(doc.new_file_counter);
}
new_file_counter += 1;
doc.filename = format!("Untitled-{new_file_counter}.txt");
doc.new_file_counter = new_file_counter;
}
pub fn add_file_path(&mut self, path: &Path) -> apperr::Result<&mut Document> {
let (path, goto) = Self::parse_filename_goto(path);
let path = path::normalize(path);
let mut file = match Self::open_for_reading(&path) {
Ok(file) => Some(file),
Err(err) if sys::apperr_is_not_found(err) => None,
Err(err) => return Err(err),
};
let file_id = match &file {
Some(file) => Some(sys::file_id(file)?),
None => None,
};
// Check if the file is already open.
if file_id.is_some() && self.update_active(|doc| doc.file_id == file_id) {
let doc = self.active_mut().unwrap();
if let Some(goto) = goto {
doc.buffer.borrow_mut().cursor_move_to_logical(goto);
}
return Ok(doc);
}
let buffer = TextBuffer::new_rc(false)?;
{
let mut tb = buffer.borrow_mut();
tb.set_margin_enabled(true);
tb.set_line_highlight_enabled(true);
if let Some(file) = &mut file {
tb.read_file(file, None)?;
if let Some(goto) = goto
&& goto != Default::default()
{
tb.cursor_move_to_logical(goto);
}
}
}
let mut doc = Document {
buffer,
path: None,
dir: None,
filename: Default::default(),
file_id,
new_file_counter: 0,
};
doc.set_path(path);
self.list.push_front(doc);
Ok(self.list.front_mut().unwrap())
}
pub fn open_for_reading(path: &Path) -> apperr::Result<File> {
File::open(path).map_err(apperr::Error::from)
}
pub fn open_for_writing(path: &Path) -> apperr::Result<File> {
File::create(path).map_err(apperr::Error::from)
}
// Parse a filename in the form of "filename:line:char".
// Returns the position of the first colon and the line/char coordinates.
fn parse_filename_goto(path: &Path) -> (&Path, Option<Point>) {
fn parse(s: &[u8]) -> Option<CoordType> {
if s.is_empty() {
return None;
}
let mut num: CoordType = 0;
for &b in s {
if !b.is_ascii_digit() {
return None;
}
let digit = (b - b'0') as CoordType;
num = num.checked_mul(10)?.checked_add(digit)?;
}
Some(num)
}
let bytes = path.as_os_str().as_encoded_bytes();
let colend = match memrchr2(b':', b':', bytes, bytes.len()) {
// Reject filenames that would result in an empty filename after stripping off the :line:char suffix.
// For instance, a filename like ":123:456" will not be processed by this function.
Some(colend) if colend > 0 => colend,
_ => return (path, None),
};
let last = match parse(&bytes[colend + 1..]) {
Some(last) => last,
None => return (path, None),
};
let last = (last - 1).max(0);
let mut len = colend;
let mut goto = Point { x: 0, y: last };
if let Some(colbeg) = memrchr2(b':', b':', bytes, colend) {
// Same here: Don't allow empty filenames.
if colbeg != 0
&& let Some(first) = parse(&bytes[colbeg + 1..colend])
{
let first = (first - 1).max(0);
len = colbeg;
goto = Point { x: last, y: first };
}
}
// Strip off the :line:char suffix.
let path = &bytes[..len];
let path = unsafe { OsStr::from_encoded_bytes_unchecked(path) };
let path = Path::new(path);
(path, Some(goto))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_last_numbers() {
fn parse(s: &str) -> (&str, Option<Point>) {
let (p, g) = DocumentManager::parse_filename_goto(Path::new(s));
(p.to_str().unwrap(), g)
}
assert_eq!(parse("123"), ("123", None));
assert_eq!(parse("abc"), ("abc", None));
assert_eq!(parse(":123"), (":123", None));
assert_eq!(parse("abc:123"), ("abc", Some(Point { x: 0, y: 122 })));
assert_eq!(parse("45:123"), ("45", Some(Point { x: 0, y: 122 })));
assert_eq!(parse(":45:123"), (":45", Some(Point { x: 0, y: 122 })));
assert_eq!(parse("abc:45:123"), ("abc", Some(Point { x: 122, y: 44 })));
assert_eq!(parse("abc:def:123"), ("abc:def", Some(Point { x: 0, y: 122 })));
assert_eq!(parse("1:2:3"), ("1", Some(Point { x: 2, y: 1 })));
assert_eq!(parse("::3"), (":", Some(Point { x: 0, y: 2 })));
assert_eq!(parse("1::3"), ("1:", Some(Point { x: 0, y: 2 })));
assert_eq!(parse(""), ("", None));
assert_eq!(parse(":"), (":", None));
assert_eq!(parse("::"), ("::", None));
assert_eq!(parse("a:1"), ("a", Some(Point { x: 0, y: 0 })));
assert_eq!(parse("1:a"), ("1:a", None));
assert_eq!(parse("file.txt:10"), ("file.txt", Some(Point { x: 0, y: 9 })));
assert_eq!(parse("file.txt:10:5"), ("file.txt", Some(Point { x: 4, y: 9 })));
}
}

View file

@ -0,0 +1,273 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::icu;
use edit::input::{kbmod, vk};
use edit::tui::*;
use crate::localization::*;
use crate::state::*;
pub fn draw_editor(ctx: &mut Context, state: &mut State) {
if !matches!(state.wants_search.kind, StateSearchKind::Hidden | StateSearchKind::Disabled) {
draw_search(ctx, state);
}
let size = ctx.size();
// TODO: The layout code should be able to just figure out the height on its own.
let height_reduction = match state.wants_search.kind {
StateSearchKind::Search => 4,
StateSearchKind::Replace => 5,
_ => 2,
};
if let Some(doc) = state.documents.active() {
ctx.textarea("textarea", doc.buffer.clone());
ctx.inherit_focus();
} else {
ctx.block_begin("empty");
ctx.block_end();
}
ctx.attr_intrinsic_size(Size { width: 0, height: size.height - height_reduction });
}
fn draw_search(ctx: &mut Context, state: &mut State) {
enum SearchAction {
None,
Search,
Replace,
ReplaceAll,
}
if let Err(err) = icu::init() {
error_log_add(ctx, state, err);
state.wants_search.kind = StateSearchKind::Disabled;
return;
}
let Some(doc) = state.documents.active() else {
state.wants_search.kind = StateSearchKind::Hidden;
return;
};
let mut action = SearchAction::None;
let mut focus = StateSearchKind::Hidden;
if state.wants_search.focus {
state.wants_search.focus = false;
focus = StateSearchKind::Search;
// If the selection is empty, focus the search input field.
// Otherwise, focus the replace input field, if it exists.
if let Some(selection) = doc.buffer.borrow_mut().extract_user_selection(false) {
state.search_needle = String::from_utf8_lossy_owned(selection);
focus = state.wants_search.kind;
}
}
ctx.block_begin("search");
ctx.attr_focus_well();
ctx.attr_background_rgba(ctx.indexed(IndexedColor::White));
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::Black));
{
if ctx.contains_focus() && ctx.consume_shortcut(vk::ESCAPE) {
state.wants_search.kind = StateSearchKind::Hidden;
}
ctx.table_begin("needle");
ctx.table_set_cell_gap(Size { width: 1, height: 0 });
{
{
ctx.table_next_row();
ctx.label("label", loc(LocId::SearchNeedleLabel));
if ctx.editline("needle", &mut state.search_needle) {
action = SearchAction::Search;
}
if !state.search_success {
ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
}
ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
if focus == StateSearchKind::Search {
ctx.steal_focus();
}
if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) {
action = SearchAction::Search;
}
}
if state.wants_search.kind == StateSearchKind::Replace {
ctx.table_next_row();
ctx.label("label", loc(LocId::SearchReplacementLabel));
ctx.editline("replacement", &mut state.search_replacement);
ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
if focus == StateSearchKind::Replace {
ctx.steal_focus();
}
if ctx.is_focused() {
if ctx.consume_shortcut(vk::RETURN) {
action = SearchAction::Replace;
} else if ctx.consume_shortcut(kbmod::CTRL_ALT | vk::RETURN) {
action = SearchAction::ReplaceAll;
}
}
}
}
ctx.table_end();
ctx.table_begin("options");
ctx.table_set_cell_gap(Size { width: 2, height: 0 });
{
ctx.table_next_row();
let mut change = false;
change |= ctx.checkbox(
"match-case",
loc(LocId::SearchMatchCase),
&mut state.search_options.match_case,
);
change |= ctx.checkbox(
"whole-word",
loc(LocId::SearchWholeWord),
&mut state.search_options.whole_word,
);
change |= ctx.checkbox(
"use-regex",
loc(LocId::SearchUseRegex),
&mut state.search_options.use_regex,
);
if change {
action = SearchAction::Search;
state.wants_search.focus = true;
ctx.needs_rerender();
}
if state.wants_search.kind == StateSearchKind::Replace
&& ctx.button("replace-all", loc(LocId::SearchReplaceAll))
{
action = SearchAction::ReplaceAll;
}
if ctx.button("close", loc(LocId::SearchClose)) {
state.wants_search.kind = StateSearchKind::Hidden;
}
}
ctx.table_end();
}
ctx.block_end();
state.search_success = match action {
SearchAction::None => return,
SearchAction::Search => {
doc.buffer.borrow_mut().find_and_select(&state.search_needle, state.search_options)
}
SearchAction::Replace => doc.buffer.borrow_mut().find_and_replace(
&state.search_needle,
state.search_options,
&state.search_replacement,
),
SearchAction::ReplaceAll => doc.buffer.borrow_mut().find_and_replace_all(
&state.search_needle,
state.search_options,
&state.search_replacement,
),
}
.is_ok();
ctx.needs_rerender();
}
pub fn draw_handle_save(ctx: &mut Context, state: &mut State) {
if let Some(doc) = state.documents.active_mut() {
if doc.path.is_some() {
if let Err(err) = doc.save(None) {
error_log_add(ctx, state, err);
}
} else {
// No path? Show the file picker.
state.wants_file_picker = StateFilePicker::SaveAs;
state.wants_save = false;
ctx.needs_rerender();
}
}
state.wants_save = false;
}
pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) {
let Some(doc) = state.documents.active() else {
state.wants_close = false;
state.wants_exit = true;
return;
};
if !doc.buffer.borrow().is_dirty() {
state.documents.remove_active();
state.wants_close = false;
ctx.needs_rerender();
return;
}
enum Action {
None,
Save,
Discard,
Cancel,
}
let mut action = Action::None;
ctx.modal_begin("unsaved-changes", loc(LocId::UnsavedChangesDialogTitle));
ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
{
ctx.label("description", loc(LocId::UnsavedChangesDialogDescription));
ctx.attr_padding(Rect::three(1, 2, 1));
ctx.table_begin("choices");
ctx.inherit_focus();
ctx.attr_padding(Rect::three(0, 2, 1));
ctx.attr_position(Position::Center);
ctx.table_set_cell_gap(Size { width: 2, height: 0 });
{
ctx.table_next_row();
ctx.inherit_focus();
if ctx.button("yes", loc(LocId::UnsavedChangesDialogYes)) {
action = Action::Save;
}
ctx.inherit_focus();
if ctx.button("no", loc(LocId::UnsavedChangesDialogNo)) {
action = Action::Discard;
}
if ctx.button("cancel", loc(LocId::Cancel)) {
action = Action::Cancel;
}
// TODO: This should highlight the corresponding letter in the label.
if ctx.consume_shortcut(vk::S) {
action = Action::Save;
} else if ctx.consume_shortcut(vk::N) {
action = Action::Discard;
}
}
ctx.table_end();
}
if ctx.modal_end() {
action = Action::Cancel;
}
match action {
Action::None => return,
Action::Save => state.wants_save = true,
Action::Discard => state.documents.remove_active(),
Action::Cancel => state.wants_exit = false,
}
state.wants_close = false;
ctx.toss_focus_up();
}

View file

@ -0,0 +1,258 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::cmp::Ordering;
use std::fs;
use std::path::PathBuf;
use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::input::vk;
use edit::tui::*;
use edit::{icu, path, sys};
use crate::localization::*;
use crate::state::*;
pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
// The save dialog is pre-filled with the current document filename.
if state.wants_file_picker == StateFilePicker::SaveAs {
state.wants_file_picker = StateFilePicker::SaveAsShown;
if state.file_picker_pending_name.as_os_str().is_empty() {
state.file_picker_pending_name =
state.documents.active().map_or("Untitled.txt", |doc| doc.filename.as_str()).into();
}
}
let width = (ctx.size().width - 20).max(10);
let height = (ctx.size().height - 10).max(10);
let mut doit = None;
let mut done = false;
ctx.modal_begin(
"file-picker",
if state.wants_file_picker == StateFilePicker::Open {
loc(LocId::FileOpen)
} else {
loc(LocId::FileSaveAs)
},
);
ctx.attr_intrinsic_size(Size { width, height });
{
let mut activated = false;
ctx.table_begin("path");
ctx.table_set_columns(&[0, COORD_TYPE_SAFE_MAX]);
ctx.table_set_cell_gap(Size { width: 1, height: 0 });
ctx.attr_padding(Rect::two(1, 1));
ctx.inherit_focus();
{
ctx.table_next_row();
ctx.label("dir-label", loc(LocId::SaveAsDialogPathLabel));
ctx.label("dir", state.file_picker_pending_dir.as_str());
ctx.attr_overflow(Overflow::TruncateMiddle);
ctx.table_next_row();
ctx.inherit_focus();
ctx.label("name-label", loc(LocId::SaveAsDialogNameLabel));
ctx.editline("name", &mut state.file_picker_pending_name);
ctx.inherit_focus();
if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) {
activated = true;
}
}
ctx.table_end();
if state.file_picker_entries.is_none() {
draw_dialog_saveas_refresh_files(state);
}
let files = state.file_picker_entries.as_ref().unwrap();
ctx.scrollarea_begin(
"directory",
Size {
width: 0,
// -1 for the label (top)
// -1 for the label (bottom)
// -1 for the editline (bottom)
height: height - 3,
},
);
ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
ctx.next_block_id_mixin(state.file_picker_pending_dir.as_str().len() as u64);
{
ctx.list_begin("files");
ctx.inherit_focus();
for entry in files {
match ctx
.list_item(state.file_picker_pending_name == entry.as_path(), entry.as_str())
{
ListSelection::Unchanged => {}
ListSelection::Selected => {
state.file_picker_pending_name = entry.as_path().into()
}
ListSelection::Activated => activated = true,
}
ctx.attr_overflow(Overflow::TruncateMiddle);
}
ctx.list_end();
if ctx.contains_focus() && ctx.consume_shortcut(vk::BACK) {
state.file_picker_pending_name = "..".into();
activated = true;
}
}
ctx.scrollarea_end();
if activated {
doit = draw_file_picker_update_path(state);
// Check if the file already exists and show an overwrite warning in that case.
if state.wants_file_picker != StateFilePicker::Open
&& let Some(path) = doit.as_deref()
&& let Some(doc) = state.documents.active()
&& let Some(file_id) = &doc.file_id
&& sys::file_id_at(path).is_ok_and(|id| &id == file_id)
{
state.file_picker_overwrite_warning = doit.take();
}
}
}
if ctx.modal_end() {
done = true;
}
if state.file_picker_overwrite_warning.is_some() {
let mut save;
ctx.modal_begin("overwrite", loc(LocId::FileOverwriteWarning));
ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
{
ctx.label("description", loc(LocId::FileOverwriteWarningDescription));
ctx.attr_overflow(Overflow::TruncateTail);
ctx.attr_padding(Rect::three(1, 2, 1));
ctx.table_begin("choices");
ctx.inherit_focus();
ctx.attr_padding(Rect::three(0, 2, 1));
ctx.attr_position(Position::Center);
ctx.table_set_cell_gap(Size { width: 2, height: 0 });
{
ctx.table_next_row();
ctx.inherit_focus();
save = ctx.button("yes", loc(LocId::Yes));
ctx.inherit_focus();
if ctx.button("no", loc(LocId::No)) {
state.file_picker_overwrite_warning = None;
}
}
ctx.table_end();
save |= ctx.consume_shortcut(vk::Y);
if ctx.consume_shortcut(vk::N) {
state.file_picker_overwrite_warning = None;
}
}
if ctx.modal_end() {
state.file_picker_overwrite_warning = None;
}
if save {
doit = state.file_picker_overwrite_warning.take();
}
}
if let Some(path) = doit {
let res = if state.wants_file_picker == StateFilePicker::Open {
state.documents.add_file_path(&path).map(|_| ())
} else if let Some(doc) = state.documents.active_mut() {
doc.save(Some(path))
} else {
Ok(())
};
match res {
Ok(..) => {
ctx.needs_rerender();
done = true;
}
Err(err) => error_log_add(ctx, state, err),
}
}
if done {
state.wants_file_picker = StateFilePicker::None;
state.file_picker_pending_name = Default::default();
state.file_picker_entries = Default::default();
state.file_picker_overwrite_warning = Default::default();
}
}
// Returns Some(path) if the path refers to a file.
fn draw_file_picker_update_path(state: &mut State) -> Option<PathBuf> {
let path = state.file_picker_pending_dir.as_path();
let path = path.join(&state.file_picker_pending_name);
let path = path::normalize(&path);
let (dir, name) = if path.is_dir() {
(path.as_path(), PathBuf::new())
} else {
let dir = path.parent().unwrap_or(&path);
let name = path.file_name().map_or(Default::default(), |s| s.into());
(dir, name)
};
if dir != state.file_picker_pending_dir.as_path() {
state.file_picker_pending_dir = DisplayablePathBuf::new(dir.to_path_buf());
state.file_picker_entries = None;
}
state.file_picker_pending_name = name;
if state.file_picker_pending_name.as_os_str().is_empty() { None } else { Some(path) }
}
fn draw_dialog_saveas_refresh_files(state: &mut State) {
let dir = state.file_picker_pending_dir.as_path();
let mut files = Vec::new();
if dir.parent().is_some() {
files.push(DisplayablePathBuf::from(".."));
}
if let Ok(iter) = fs::read_dir(dir) {
for entry in iter.flatten() {
if let Ok(metadata) = entry.metadata() {
let mut name = entry.file_name();
if metadata.is_dir()
|| (metadata.is_symlink()
&& fs::metadata(entry.path()).is_ok_and(|m| m.is_dir()))
{
name.push("/");
}
files.push(DisplayablePathBuf::from(name));
}
}
}
// Sort directories first, then by name, case-insensitive.
let off = files.len().saturating_sub(1);
files[off..].sort_by(|a, b| {
let a = a.as_bytes();
let b = b.as_bytes();
let a_is_dir = a.last() == Some(&b'/');
let b_is_dir = b.last() == Some(&b'/');
match b_is_dir.cmp(&a_is_dir) {
Ordering::Equal => icu::compare_strings(a, b),
other => other,
}
});
state.file_picker_entries = Some(files);
}

View file

@ -0,0 +1,161 @@
// 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 crate::localization::*;
use crate::state::*;
pub fn draw_menubar(ctx: &mut Context, state: &mut State) {
ctx.menubar_begin();
ctx.attr_background_rgba(state.menubar_color_bg);
ctx.attr_foreground_rgba(state.menubar_color_fg);
{
if ctx.menubar_menu_begin(loc(LocId::File), 'F') {
draw_menu_file(ctx, state);
}
if state.documents.active().is_some() && ctx.menubar_menu_begin(loc(LocId::Edit), 'E') {
draw_menu_edit(ctx, state);
}
if ctx.menubar_menu_begin(loc(LocId::View), 'V') {
draw_menu_view(ctx, state);
}
if ctx.menubar_menu_begin(loc(LocId::Help), 'H') {
draw_menu_help(ctx, state);
}
}
ctx.menubar_end();
}
fn draw_menu_file(ctx: &mut Context, state: &mut State) {
if ctx.menubar_menu_button(loc(LocId::FileNew), 'N', kbmod::CTRL | vk::N) {
draw_add_untitled_document(ctx, state);
}
if ctx.menubar_menu_button(loc(LocId::FileOpen), 'O', kbmod::CTRL | vk::O) {
state.wants_file_picker = StateFilePicker::Open;
}
if state.documents.active().is_some() {
if ctx.menubar_menu_button(loc(LocId::FileSave), 'S', kbmod::CTRL | vk::S) {
state.wants_save = true;
}
if ctx.menubar_menu_button(loc(LocId::FileSaveAs), 'A', vk::NULL) {
state.wants_file_picker = StateFilePicker::SaveAs;
}
}
if ctx.menubar_menu_button(loc(LocId::FileClose), 'C', kbmod::CTRL | vk::W) {
state.wants_close = true;
}
if ctx.menubar_menu_button(loc(LocId::FileExit), 'X', kbmod::CTRL | vk::Q) {
state.wants_exit = true;
}
ctx.menubar_menu_end();
}
fn draw_menu_edit(ctx: &mut Context, state: &mut State) {
let doc = state.documents.active().unwrap();
let mut tb = doc.buffer.borrow_mut();
if ctx.menubar_menu_button(loc(LocId::EditUndo), 'U', kbmod::CTRL | vk::Z) {
tb.undo();
ctx.needs_rerender();
}
if ctx.menubar_menu_button(loc(LocId::EditRedo), 'R', kbmod::CTRL | vk::Y) {
tb.redo();
ctx.needs_rerender();
}
if ctx.menubar_menu_button(loc(LocId::EditCut), 'T', kbmod::CTRL | vk::X) {
ctx.set_clipboard(tb.extract_selection(true));
}
if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) {
ctx.set_clipboard(tb.extract_selection(false));
}
if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) {
tb.write(ctx.clipboard(), true);
ctx.needs_rerender();
}
if state.wants_search.kind != StateSearchKind::Disabled {
if ctx.menubar_menu_button(loc(LocId::EditFind), 'F', kbmod::CTRL | vk::F) {
state.wants_search.kind = StateSearchKind::Search;
state.wants_search.focus = true;
}
if ctx.menubar_menu_button(loc(LocId::EditReplace), 'R', kbmod::CTRL | vk::R) {
state.wants_search.kind = StateSearchKind::Replace;
state.wants_search.focus = true;
}
}
ctx.menubar_menu_end();
}
fn draw_menu_view(ctx: &mut Context, state: &mut State) {
if ctx.menubar_menu_button(loc(LocId::ViewFocusStatusbar), 'S', vk::NULL) {
state.wants_statusbar_focus = true;
}
if let Some(doc) = state.documents.active() {
let mut tb = doc.buffer.borrow_mut();
let word_wrap = tb.is_word_wrap_enabled();
if ctx.menubar_menu_checkbox(loc(LocId::ViewWordWrap), 'W', kbmod::ALT | vk::Z, word_wrap) {
tb.set_word_wrap(!word_wrap);
ctx.needs_rerender();
}
}
ctx.menubar_menu_end();
}
fn draw_menu_help(ctx: &mut Context, state: &mut State) {
if ctx.menubar_menu_button(loc(LocId::HelpAbout), 'A', vk::NULL) {
state.wants_about = true;
}
ctx.menubar_menu_end();
}
pub fn draw_dialog_about(ctx: &mut Context, state: &mut State) {
ctx.modal_begin("about", loc(LocId::AboutDialogTitle));
{
ctx.block_begin("content");
ctx.inherit_focus();
ctx.attr_padding(Rect::three(1, 2, 1));
{
ctx.label("description", "Microsoft Edit");
ctx.attr_overflow(Overflow::TruncateTail);
ctx.attr_position(Position::Center);
ctx.label(
"version",
&arena_format!(
ctx.arena(),
"{}{}",
loc(LocId::AboutDialogVersion),
env!("CARGO_PKG_VERSION")
),
);
ctx.attr_overflow(Overflow::TruncateHead);
ctx.attr_position(Position::Center);
ctx.label("copyright", "Copyright (c) Microsoft Corp 2025");
ctx.attr_overflow(Overflow::TruncateTail);
ctx.attr_position(Position::Center);
ctx.block_begin("choices");
ctx.inherit_focus();
ctx.attr_padding(Rect::three(1, 2, 0));
ctx.attr_position(Position::Center);
{
if ctx.button("ok", loc(LocId::Ok)) {
state.wants_about = false;
}
ctx.inherit_focus();
}
ctx.block_end();
}
ctx.block_end();
}
if ctx.modal_end() {
state.wants_about = false;
}
}

View file

@ -0,0 +1,291 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use edit::framebuffer::{Attributes, IndexedColor};
use edit::helpers::*;
use edit::input::vk;
use edit::tui::*;
use edit::{arena_format, icu};
use crate::localization::*;
use crate::state::*;
pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
ctx.table_begin("statusbar");
ctx.attr_focus_well();
ctx.attr_background_rgba(state.menubar_color_bg);
ctx.attr_foreground_rgba(state.menubar_color_fg);
ctx.table_set_cell_gap(Size { width: 2, height: 0 });
ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
ctx.attr_padding(Rect::two(0, 1));
if let Some(doc) = state.documents.active() {
let mut tb = doc.buffer.borrow_mut();
ctx.table_next_row();
if ctx.button("newline", if tb.is_crlf() { "CRLF" } else { "LF" }) {
let is_crlf = tb.is_crlf();
tb.normalize_newlines(!is_crlf);
}
if state.wants_statusbar_focus {
state.wants_statusbar_focus = false;
ctx.steal_focus();
}
state.wants_encoding_picker |= ctx.button("encoding", tb.encoding());
if state.wants_encoding_picker {
if doc.path.is_some() {
ctx.block_begin("frame");
ctx.attr_float(FloatSpec {
anchor: Anchor::Last,
gravity_x: 0.0,
gravity_y: 1.0,
offset_x: 0.0,
offset_y: 0.0,
});
ctx.attr_padding(Rect::two(0, 1));
ctx.attr_border();
{
if ctx.button("reopen", loc(LocId::EncodingReopen)) {
state.wants_encoding_change = StateEncodingChange::Reopen;
}
ctx.focus_on_first_present();
if ctx.button("convert", loc(LocId::EncodingConvert)) {
state.wants_encoding_change = StateEncodingChange::Convert;
}
}
ctx.block_end();
} else {
// Can't reopen a file that doesn't exist.
state.wants_encoding_change = StateEncodingChange::Convert;
}
if !ctx.contains_focus() {
state.wants_encoding_picker = false;
ctx.needs_rerender();
}
}
state.wants_indentation_picker |= ctx.button(
"indentation",
&arena_format!(
ctx.arena(),
"{}:{}",
loc(if tb.indent_with_tabs() {
LocId::IndentationTabs
} else {
LocId::IndentationSpaces
}),
tb.tab_size(),
),
);
if state.wants_indentation_picker {
ctx.table_begin("indentation-picker");
ctx.attr_float(FloatSpec {
anchor: Anchor::Last,
gravity_x: 0.0,
gravity_y: 1.0,
offset_x: 0.0,
offset_y: 0.0,
});
ctx.attr_border();
ctx.attr_padding(Rect::two(0, 1));
ctx.table_set_cell_gap(Size { width: 1, height: 0 });
{
if ctx.consume_shortcut(vk::RETURN) {
ctx.toss_focus_up();
}
ctx.table_next_row();
ctx.list_begin("type");
ctx.focus_on_first_present();
ctx.attr_padding(Rect::two(0, 1));
{
if ctx.list_item(tb.indent_with_tabs(), loc(LocId::IndentationTabs))
!= ListSelection::Unchanged
{
tb.set_indent_with_tabs(true);
ctx.needs_rerender();
}
if ctx.list_item(!tb.indent_with_tabs(), loc(LocId::IndentationSpaces))
!= ListSelection::Unchanged
{
tb.set_indent_with_tabs(false);
ctx.needs_rerender();
}
}
ctx.list_end();
ctx.list_begin("width");
ctx.attr_padding(Rect::two(0, 2));
{
for width in 1u8..=8 {
let ch = [b'0' + width];
let label = unsafe { std::str::from_utf8_unchecked(&ch) };
if ctx.list_item(tb.tab_size() == width as CoordType, label)
!= ListSelection::Unchanged
{
tb.set_tab_size(width as CoordType);
ctx.needs_rerender();
}
}
}
ctx.list_end();
}
ctx.table_end();
if !ctx.contains_focus() {
state.wants_indentation_picker = false;
ctx.needs_rerender();
}
}
ctx.label(
"location",
&arena_format!(
ctx.arena(),
"{}:{}",
tb.cursor_logical_pos().y + 1,
tb.cursor_logical_pos().x + 1
),
);
#[cfg(any(feature = "debug-layout", feature = "debug-latency"))]
ctx.label(
"stats",
&arena_format!(ctx.arena(), "{}/{}", tb.logical_line_count(), tb.visual_line_count(),),
);
if tb.is_overtype() && ctx.button("overtype", "OVR") {
tb.set_overtype(false);
ctx.needs_rerender();
}
if tb.is_dirty() {
ctx.label("dirty", "*");
}
ctx.block_begin("filename-container");
ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
{
let total = state.documents.len();
let mut filename = doc.filename.as_str();
let filename_buf;
if total > 1 {
filename_buf = arena_format!(ctx.arena(), "{} + {}", filename, total - 1);
filename = &filename_buf;
}
state.wants_document_picker |= ctx.button("filename", filename);
ctx.inherit_focus();
ctx.attr_overflow(Overflow::TruncateMiddle);
ctx.attr_position(Position::Right);
}
ctx.block_end();
}
ctx.table_end();
}
pub fn draw_dialog_encoding_change(ctx: &mut Context, state: &mut State) {
let doc = state.documents.active_mut().unwrap();
let reopen = state.wants_encoding_change == StateEncodingChange::Reopen;
let width = (ctx.size().width - 20).max(10);
let height = (ctx.size().height - 10).max(10);
let mut change = None;
ctx.modal_begin(
"encode",
if reopen { loc(LocId::EncodingReopen) } else { loc(LocId::EncodingConvert) },
);
{
ctx.scrollarea_begin("scrollarea", Size { width, height });
ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
ctx.inherit_focus();
{
let encodings = icu::get_available_encodings();
ctx.list_begin("encodings");
ctx.inherit_focus();
for &encoding in encodings {
if ctx.list_item(encoding == doc.buffer.borrow().encoding(), encoding)
== ListSelection::Activated
{
change = Some(encoding);
break;
}
}
ctx.list_end();
}
ctx.scrollarea_end();
}
if ctx.modal_end() {
state.wants_encoding_change = StateEncodingChange::None;
}
if let Some(encoding) = change {
if reopen && doc.path.is_some() {
let mut res = Ok(());
if doc.buffer.borrow().is_dirty() {
res = doc.save(None);
}
if res.is_ok() {
res = doc.reread(Some(encoding));
}
if let Err(err) = res {
error_log_add(ctx, state, err);
}
} else {
doc.buffer.borrow_mut().set_encoding(encoding);
}
state.wants_encoding_change = StateEncodingChange::None;
ctx.needs_rerender();
}
}
pub fn draw_document_picker(ctx: &mut Context, state: &mut State) {
ctx.modal_begin("document-picker", "");
{
let width = (ctx.size().width - 20).max(10);
let height = (ctx.size().height - 10).max(10);
ctx.scrollarea_begin("scrollarea", Size { width, height });
ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
ctx.inherit_focus();
{
ctx.list_begin("documents");
ctx.inherit_focus();
if state.documents.update_active(|doc| {
let tb = doc.buffer.borrow();
ctx.styled_list_item_begin();
ctx.attr_overflow(Overflow::TruncateTail);
ctx.styled_label_add_text(if tb.is_dirty() { "* " } else { " " });
ctx.styled_label_add_text(&doc.filename);
if let Some(path) = &doc.dir {
ctx.styled_label_add_text(" ");
ctx.styled_label_set_attributes(Attributes::Italic);
ctx.styled_label_add_text(path.as_str());
}
ctx.styled_list_item_end(false) == ListSelection::Activated
}) {
state.wants_document_picker = false;
ctx.needs_rerender();
}
ctx.list_end();
}
ctx.scrollarea_end();
}
if ctx.modal_end() {
state.wants_document_picker = false;
}
}

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<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"
manifestVersion="1.0">
<asmv3:application>
<windowsSettings>
<ws2:longPathAware>true</ws2:longPathAware>
<ws3:activeCodePage>UTF-8</ws3:activeCodePage>
<ws4:heapType>SegmentHeap</ws4:heapType>
</windowsSettings>
</asmv3:application>
<cv1:compatibility>
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</cv1:compatibility>
</assembly>

View file

@ -0,0 +1,950 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use edit::arena::scratch_arena;
use edit::sys;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LocId {
Ctrl,
Alt,
Shift,
Ok,
Yes,
No,
Cancel,
Always,
// File menu
File,
FileNew,
FileOpen,
FileSave,
FileSaveAs,
FileClose,
FileExit,
// Edit menu
Edit,
EditUndo,
EditRedo,
EditCut,
EditCopy,
EditPaste,
EditFind,
EditReplace,
// View menu
View,
ViewFocusStatusbar,
ViewWordWrap,
// Help menu
Help,
HelpAbout,
// Exit dialog
UnsavedChangesDialogTitle,
UnsavedChangesDialogDescription,
UnsavedChangesDialogYes,
UnsavedChangesDialogNo,
// About dialog
AboutDialogTitle,
AboutDialogVersion,
// Shown when the clipboard size exceeds the limit for OSC 52
LargeClipboardWarningLine1,
LargeClipboardWarningLine2,
LargeClipboardWarningLine3,
SuperLargeClipboardWarning,
// Warning dialog
WarningDialogTitle,
// Error dialog
ErrorDialogTitle,
ErrorIcuMissing,
SearchNeedleLabel,
SearchReplacementLabel,
SearchMatchCase,
SearchWholeWord,
SearchUseRegex,
SearchReplaceAll,
SearchClose,
EncodingReopen,
EncodingConvert,
IndentationTabs,
IndentationSpaces,
SaveAsDialogPathLabel,
SaveAsDialogNameLabel,
FileOverwriteWarning,
FileOverwriteWarningDescription,
Count,
}
#[allow(non_camel_case_types)]
#[derive(Clone, Copy, PartialEq, Eq)]
enum LangId {
// Base language. It's always the first one.
en,
// Other languages. Sorted alphabetically.
de,
es,
fr,
it,
ja,
ko,
pt_br,
ru,
zh_hans,
zh_hant,
Count,
}
#[rustfmt::skip]
const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
// Ctrl (the keyboard key)
[
/* en */ "Ctrl",
/* de */ "Strg",
/* es */ "Ctrl",
/* fr */ "Ctrl",
/* it */ "Ctrl",
/* ja */ "Ctrl",
/* ko */ "Ctrl",
/* pt_br */ "Ctrl",
/* ru */ "Ctrl",
/* zh_hans */ "Ctrl",
/* zh_hant */ "Ctrl",
],
// Alt (the keyboard key)
[
/* en */ "Alt",
/* de */ "Alt",
/* es */ "Alt",
/* fr */ "Alt",
/* it */ "Alt",
/* ja */ "Alt",
/* ko */ "Alt",
/* pt_br */ "Alt",
/* ru */ "Alt",
/* zh_hans */ "Alt",
/* zh_hant */ "Alt",
],
// Shift (the keyboard key)
[
/* en */ "Shift",
/* de */ "Umschalt",
/* es */ "Mayús",
/* fr */ "Maj",
/* it */ "Maiusc",
/* ja */ "Shift",
/* ko */ "Shift",
/* pt_br */ "Shift",
/* ru */ "Shift",
/* zh_hans */ "Shift",
/* zh_hant */ "Shift",
],
// Ok (used as a common dialog button)
[
/* en */ "Ok",
/* de */ "OK",
/* es */ "Aceptar",
/* fr */ "OK",
/* it */ "OK",
/* ja */ "OK",
/* ko */ "확인",
/* pt_br */ "OK",
/* ru */ "ОК",
/* zh_hans */ "确定",
/* zh_hant */ "確定",
],
// Yes (used as a common dialog button)
[
/* en */ "Yes",
/* de */ "Ja",
/* es */ "",
/* fr */ "Oui",
/* it */ "",
/* ja */ "はい",
/* ko */ "",
/* pt_br */ "Sim",
/* ru */ "Да",
/* zh_hans */ "",
/* zh_hant */ "",
],
// No (used as a common dialog button)
[
/* en */ "No",
/* de */ "Nein",
/* es */ "No",
/* fr */ "Non",
/* it */ "No",
/* ja */ "いいえ",
/* ko */ "아니오",
/* pt_br */ "Não",
/* ru */ "Нет",
/* zh_hans */ "",
/* zh_hant */ "",
],
// Cancel (used as a common dialog button)
[
/* en */ "Cancel",
/* de */ "Abbrechen",
/* es */ "Cancelar",
/* fr */ "Annuler",
/* it */ "Annulla",
/* ja */ "キャンセル",
/* ko */ "취소",
/* pt_br */ "Cancelar",
/* ru */ "Отмена",
/* zh_hans */ "取消",
/* zh_hant */ "取消",
],
// Always (used as a common dialog button)
[
/* en */ "Always",
/* de */ "Immer",
/* es */ "Siempre",
/* fr */ "Toujours",
/* it */ "Sempre",
/* ja */ "常に",
/* ko */ "항상",
/* pt_br */ "Sempre",
/* ru */ "Всегда",
/* zh_hans */ "总是",
/* zh_hant */ "總是",
],
// File (a menu bar item)
[
/* en */ "File",
/* de */ "Datei",
/* es */ "Archivo",
/* fr */ "Fichier",
/* it */ "File",
/* ja */ "ファイル",
/* ko */ "파일",
/* pt_br */ "Arquivo",
/* ru */ "Файл",
/* zh_hans */ "文件",
/* zh_hant */ "檔案",
],
// FileNew
[
/* en */ "New File…",
/* de */ "Neue Datei…",
/* es */ "Nuevo archivo…",
/* fr */ "Nouveau fichier…",
/* it */ "Nuovo file…",
/* ja */ "新規ファイル…",
/* ko */ "새 파일…",
/* pt_br */ "Novo arquivo…",
/* ru */ "Новый файл…",
/* zh_hans */ "新建文件…",
/* zh_hant */ "新增檔案…",
],
// FileOpen
[
/* en */ "Open File…",
/* de */ "Datei öffnen…",
/* es */ "Abrir archivo…",
/* fr */ "Ouvrir un fichier…",
/* it */ "Apri file…",
/* ja */ "ファイルを開く…",
/* ko */ "파일 열기…",
/* pt_br */ "Abrir arquivo…",
/* ru */ "Открыть файл…",
/* zh_hans */ "打开文件…",
/* zh_hant */ "開啟檔案…",
],
// FileSave
[
/* en */ "Save",
/* de */ "Speichern",
/* es */ "Guardar",
/* fr */ "Enregistrer",
/* it */ "Salva",
/* ja */ "保存",
/* ko */ "저장",
/* pt_br */ "Salvar",
/* ru */ "Сохранить",
/* zh_hans */ "保存",
/* zh_hant */ "儲存",
],
// FileSaveAs
[
/* en */ "Save As…",
/* de */ "Speichern unter…",
/* es */ "Guardar como…",
/* fr */ "Enregistrer sous…",
/* it */ "Salva come…",
/* ja */ "名前を付けて保存…",
/* ko */ "다른 이름으로 저장…",
/* pt_br */ "Salvar como…",
/* ru */ "Сохранить как…",
/* zh_hans */ "另存为…",
/* zh_hant */ "另存新檔…",
],
// FileClose
[
/* en */ "Close Editor",
/* de */ "Editor schließen",
/* es */ "Cerrar editor",
/* fr */ "Fermer l'éditeur",
/* it */ "Chiudi editor",
/* ja */ "エディターを閉じる",
/* ko */ "편집기 닫기",
/* pt_br */ "Fechar editor",
/* ru */ "Закрыть редактор",
/* zh_hans */ "关闭编辑器",
/* zh_hant */ "關閉編輯器",
],
// FileExit
[
/* en */ "Exit",
/* de */ "Beenden",
/* es */ "Salir",
/* fr */ "Quitter",
/* it */ "Esci",
/* ja */ "終了",
/* ko */ "종료",
/* pt_br */ "Sair",
/* ru */ "Выход",
/* zh_hans */ "退出",
/* zh_hant */ "退出",
],
// Edit (a menu bar item)
[
/* en */ "Edit",
/* de */ "Bearbeiten",
/* es */ "Editar",
/* fr */ "Éditer",
/* it */ "Modifica",
/* ja */ "編集",
/* ko */ "편집",
/* pt_br */ "Editar",
/* ru */ "Правка",
/* zh_hans */ "编辑",
/* zh_hant */ "編輯",
],
// EditUndo
[
/* en */ "Undo",
/* de */ "Rückgängig",
/* es */ "Deshacer",
/* fr */ "Annuler",
/* it */ "Annulla",
/* ja */ "元に戻す",
/* ko */ "실행 취소",
/* pt_br */ "Desfazer",
/* ru */ "Отменить",
/* zh_hans */ "撤销",
/* zh_hant */ "復原",
],
// EditRedo
[
/* en */ "Redo",
/* de */ "Wiederholen",
/* es */ "Rehacer",
/* fr */ "Rétablir",
/* it */ "Ripeti",
/* ja */ "やり直し",
/* ko */ "다시 실행",
/* pt_br */ "Refazer",
/* ru */ "Повторить",
/* zh_hans */ "重做",
/* zh_hant */ "重做",
],
// EditCut
[
/* en */ "Cut",
/* de */ "Ausschneiden",
/* es */ "Cortar",
/* fr */ "Couper",
/* it */ "Taglia",
/* ja */ "切り取り",
/* ko */ "잘라내기",
/* pt_br */ "Cortar",
/* ru */ "Вырезать",
/* zh_hans */ "剪切",
/* zh_hant */ "剪下",
],
// EditCopy
[
/* en */ "Copy",
/* de */ "Kopieren",
/* es */ "Copiar",
/* fr */ "Copier",
/* it */ "Copia",
/* ja */ "コピー",
/* ko */ "복사",
/* pt_br */ "Copiar",
/* ru */ "Копировать",
/* zh_hans */ "复制",
/* zh_hant */ "複製",
],
// EditPaste
[
/* en */ "Paste",
/* de */ "Einfügen",
/* es */ "Pegar",
/* fr */ "Coller",
/* it */ "Incolla",
/* ja */ "貼り付け",
/* ko */ "붙여넣기",
/* pt_br */ "Colar",
/* ru */ "Вставить",
/* zh_hans */ "粘贴",
/* zh_hant */ "貼上",
],
// EditFind
[
/* en */ "Find",
/* de */ "Suchen",
/* es */ "Buscar",
/* fr */ "Rechercher",
/* it */ "Trova",
/* ja */ "検索",
/* ko */ "찾기",
/* pt_br */ "Encontrar",
/* ru */ "Найти",
/* zh_hans */ "查找",
/* zh_hant */ "尋找",
],
// EditReplace
[
/* en */ "Replace",
/* de */ "Ersetzen",
/* es */ "Reemplazar",
/* fr */ "Remplacer",
/* it */ "Sostituisci",
/* ja */ "置換",
/* ko */ "바꾸기",
/* pt_br */ "Substituir",
/* ru */ "Заменить",
/* zh_hans */ "替换",
/* zh_hant */ "取代",
],
// View (a menu bar item)
[
/* en */ "View",
/* de */ "Ansicht",
/* es */ "Ver",
/* fr */ "Affichage",
/* it */ "Visualizza",
/* ja */ "表示",
/* ko */ "보기",
/* pt_br */ "Exibir",
/* ru */ "Вид",
/* zh_hans */ "视图",
/* zh_hant */ "檢視",
],
// ViewFocusStatusbar
[
/* en */ "Focus Statusbar",
/* de */ "Statusleiste fokussieren",
/* es */ "Enfocar barra de estado",
/* fr */ "Focus sur la barre d'état",
/* it */ "Attiva barra di stato",
/* ja */ "ステータスバーにフォーカス",
/* ko */ "상태 표시줄로 포커스 이동",
/* pt_br */ "Focar barra de status",
/* ru */ "Фокус на строку состояния",
/* zh_hans */ "聚焦状态栏",
/* zh_hant */ "聚焦狀態列",
],
// ViewWordWrap
[
/* en */ "Word Wrap",
/* de */ "Zeilenumbruch",
/* es */ "Ajuste de línea",
/* fr */ "Retour à la ligne",
/* it */ "A capo automatico",
/* ja */ "折り返し",
/* ko */ "자동 줄 바꿈",
/* pt_br */ "Quebra de linha",
/* ru */ "Перенос слов",
/* zh_hans */ "自动换行",
/* zh_hant */ "自動換行",
],
// Help (a menu bar item)
[
/* en */ "Help",
/* de */ "Hilfe",
/* es */ "Ayuda",
/* fr */ "Aide",
/* it */ "Aiuto",
/* ja */ "ヘルプ",
/* ko */ "도움말",
/* pt_br */ "Ajuda",
/* ru */ "Помощь",
/* zh_hans */ "帮助",
/* zh_hant */ "幫助",
],
// HelpAbout
[
/* en */ "About",
/* de */ "Über",
/* es */ "Acerca de",
/* fr */ "À propos",
/* it */ "Informazioni",
/* ja */ "情報",
/* ko */ "정보",
/* pt_br */ "Sobre",
/* ru */ "О программе",
/* zh_hans */ "关于",
/* zh_hant */ "關於",
],
// UnsavedChangesDialogTitle
[
/* en */ "Unsaved Changes",
/* de */ "Ungespeicherte Änderungen",
/* es */ "Cambios sin guardar",
/* fr */ "Modifications non enregistrées",
/* it */ "Modifiche non salvate",
/* ja */ "未保存の変更",
/* ko */ "저장되지 않은 변경 사항",
/* pt_br */ "Alterações não salvas",
/* ru */ "Несохраненные изменения",
/* zh_hans */ "未保存的更改",
/* zh_hant */ "未儲存的變更",
],
// UnsavedChangesDialogDescription
[
/* en */ "Do you want to save the changes you made?",
/* de */ "Möchten Sie die vorgenommenen Änderungen speichern?",
/* es */ "¿Desea guardar los cambios realizados?",
/* fr */ "Voulez-vous enregistrer les modifications apportées?",
/* it */ "Vuoi salvare le modifiche apportate?",
/* ja */ "変更内容を保存しますか?",
/* ko */ "변경한 내용을 저장하시겠습니까?",
/* pt_br */ "Deseja salvar as alterações feitas?",
/* ru */ "Вы хотите сохранить внесённые изменения?",
/* zh_hans */ "您要保存所做的更改吗?",
/* zh_hant */ "您要保存所做的變更嗎?",
],
// UnsavedChangesDialogYes
[
/* en */ "Save",
/* de */ "Speichern",
/* es */ "Guardar",
/* fr */ "Enregistrer",
/* it */ "Salva",
/* ja */ "保存",
/* ko */ "저장",
/* pt_br */ "Salvar",
/* ru */ "Сохранить",
/* zh_hans */ "保存",
/* zh_hant */ "儲存",
],
// UnsavedChangesDialogNo
[
/* en */ "Don't Save",
/* de */ "Nicht speichern",
/* es */ "No guardar",
/* fr */ "Ne pas enregistrer",
/* it */ "Non salvare",
/* ja */ "保存しない",
/* ko */ "저장 안 함",
/* pt_br */ "Não salvar",
/* ru */ "Не сохранять",
/* zh_hans */ "不保存",
/* zh_hant */ "不儲存",
],
// AboutDialogTitle
[
/* en */ "About",
/* de */ "Über",
/* es */ "Acerca de",
/* fr */ "À propos",
/* it */ "Informazioni",
/* ja */ "情報",
/* ko */ "정보",
/* pt_br */ "Sobre",
/* ru */ "О программе",
/* zh_hans */ "关于",
/* zh_hant */ "關於",
],
// AboutDialogVersion
[
/* en */ "Version: ",
/* de */ "Version: ",
/* es */ "Versión: ",
/* fr */ "Version: ",
/* it */ "Versione: ",
/* ja */ "バージョン: ",
/* ko */ "버전: ",
/* pt_br */ "Versão: ",
/* ru */ "Версия: ",
/* zh_hans */ "版本: ",
/* zh_hant */ "版本: ",
],
// Shown when the clipboard size exceeds the limit for OSC 52
// LargeClipboardWarningLine1
[
/* en */ "Text you copy is shared with the terminal clipboard.",
/* de */ "Der kopierte Text wird mit der Terminal-Zwischenablage geteilt.",
/* es */ "El texto que copies se comparte con el portapapeles del terminal.",
/* fr */ "Le texte que vous copiez est partagé avec le presse-papiers du terminal.",
/* it */ "Il testo copiato viene condiviso con gli appunti del terminale.",
/* ja */ "コピーしたテキストはターミナルのクリップボードと共有されます。",
/* ko */ "복사한 텍스트가 터미널 클립보드와 공유됩니다.",
/* pt_br */ "O texto copiado é compartilhado com a área de transferência do terminal.",
/* ru */ "Скопированный текст передаётся в буфер обмена терминала.",
/* zh_hans */ "你复制的文本将共享到终端剪贴板。",
/* zh_hant */ "您複製的文字將會與終端機剪貼簿分享。",
],
// LargeClipboardWarningLine2
[
/* en */ "You copied {size} which may take a long time to share.",
/* de */ "Sie haben {size} kopiert, das Weitergeben könnte lange dauern.",
/* es */ "Copiaste {size}, lo que puede tardar en compartirse.",
/* fr */ "Vous avez copié {size}, ce qui peut être long à partager.",
/* it */ "Hai copiato {size}, potrebbe richiedere molto tempo per condividerlo.",
/* ja */ "{size} をコピーしました。共有に時間がかかる可能性があります。",
/* ko */ "{size}를 복사했습니다. 공유하는 데 시간이 오래 걸릴 수 있습니다.",
/* pt_br */ "Você copiou {size}, o que pode demorar para compartilhar.",
/* ru */ "Вы скопировали {size}; передача может занять много времени.",
/* zh_hans */ "你复制了 {size},共享可能需要较长时间。",
/* zh_hant */ "您已複製 {size},共享可能需要較長時間。",
],
// LargeClipboardWarningLine3
[
/* en */ "Do you want to send it anyway?",
/* de */ "Möchten Sie es trotzdem senden?",
/* es */ "¿Desea enviarlo de todas formas?",
/* fr */ "Voulez-vous quand même lenvoyer?",
/* it */ "Vuoi inviarlo comunque?",
/* ja */ "それでも送信しますか?",
/* ko */ "그래도 전송하시겠습니까?",
/* pt_br */ "Deseja enviar mesmo assim?",
/* ru */ "Отправить в любом случае?",
/* zh_hans */ "仍要发送吗?",
/* zh_hant */ "仍要傳送嗎?",
],
// SuperLargeClipboardWarning (as an alternative to LargeClipboardWarningLine2 and 3)
[
/* en */ "The text you copied is too large to be shared.",
/* de */ "Der kopierte Text ist zu groß, um geteilt zu werden.",
/* es */ "El texto que copiaste es demasiado grande para compartirse.",
/* fr */ "Le texte que vous avez copié est trop volumineux pour être partagé.",
/* it */ "Il testo copiato è troppo grande per essere condiviso.",
/* ja */ "コピーしたテキストは大きすぎて共有できません。",
/* ko */ "복사한 텍스트가 너무 커서 공유할 수 없습니다.",
/* pt_br */ "O texto copiado é grande demais para ser compartilhado.",
/* ru */ "Скопированный текст слишком велик для передачи.",
/* zh_hans */ "你复制的文本过大,无法共享。",
/* zh_hant */ "您複製的文字過大,無法分享。",
],
// WarningDialogTitle
[
/* en */ "Warning",
/* de */ "Warnung",
/* es */ "Advertencia",
/* fr */ "Avertissement",
/* it */ "Avviso",
/* ja */ "警告",
/* ko */ "경고",
/* pt_br */ "Aviso",
/* ru */ "Предупреждение",
/* zh_hans */ "警告",
/* zh_hant */ "警告",
],
// ErrorDialogTitle
[
/* en */ "Error",
/* de */ "Fehler",
/* es */ "Error",
/* fr */ "Erreur",
/* it */ "Errore",
/* ja */ "エラー",
/* ko */ "오류",
/* pt_br */ "Erro",
/* ru */ "Ошибка",
/* zh_hans */ "错误",
/* zh_hant */ "錯誤",
],
// ErrorIcuMissing
[
/* en */ "This operation requires the ICU library",
/* de */ "Diese Operation erfordert die ICU-Bibliothek",
/* es */ "Esta operación requiere la biblioteca ICU",
/* fr */ "Cette opération nécessite la bibliothèque ICU",
/* it */ "Questa operazione richiede la libreria ICU",
/* ja */ "この操作にはICUライブラリが必要です",
/* ko */ "이 작업에는 ICU 라이브러리가 필요합니다",
/* pt_br */ "Esta operação requer a biblioteca ICU",
/* ru */ "Эта операция требует наличия библиотеки ICU",
/* zh_hans */ "此操作需要 ICU 库",
/* zh_hant */ "此操作需要 ICU 庫",
],
// SearchNeedleLabel (for input field)
[
/* en */ "Find:",
/* de */ "Suchen:",
/* es */ "Buscar:",
/* fr */ "Rechercher:",
/* it */ "Trova:",
/* ja */ "検索:",
/* ko */ "찾기:",
/* pt_br */ "Encontrar:",
/* ru */ "Найти:",
/* zh_hans */ "查找:",
/* zh_hant */ "尋找:",
],
// SearchReplacementLabel (for input field)
[
/* en */ "Replace:",
/* de */ "Ersetzen:",
/* es */ "Reemplazar:",
/* fr */ "Remplacer:",
/* it */ "Sostituire:",
/* ja */ "置換:",
/* ko */ "바꾸기:",
/* pt_br */ "Substituir:",
/* ru */ "Замена:",
/* zh_hans */ "替换:",
/* zh_hant */ "替換:",
],
// SearchMatchCase (toggle)
[
/* en */ "Match Case",
/* de */ "Groß/Klein",
/* es */ "May/Min",
/* fr */ "Casse",
/* it */ "Maius/minus",
/* ja */ "大/小文字",
/* ko */ "대소문자",
/* pt_br */ "Maius/minus",
/* ru */ "Регистр",
/* zh_hans */ "区分大小写",
/* zh_hant */ "區分大小寫",
],
// SearchWholeWord (toggle)
[
/* en */ "Whole Word",
/* de */ "Ganzes Wort",
/* es */ "Palabra",
/* fr */ "Mot entier",
/* it */ "Parola",
/* ja */ "単語単位",
/* ko */ "전체 단어",
/* pt_br */ "Palavra",
/* ru */ "Слово",
/* zh_hans */ "全字匹配",
/* zh_hant */ "全字匹配",
],
// SearchUseRegex (toggle)
[
/* en */ "Use Regex",
/* de */ "RegEx",
/* es */ "RegEx",
/* fr */ "RegEx",
/* it */ "RegEx",
/* ja */ "正規表現",
/* ko */ "정규식",
/* pt_br */ "RegEx",
/* ru */ "RegEx",
/* zh_hans */ "正则",
/* zh_hant */ "正則",
],
// SearchReplaceAll (button)
[
/* en */ "Replace All",
/* de */ "Alle ersetzen",
/* es */ "Reemplazar todo",
/* fr */ "Remplacer tout",
/* it */ "Sostituisci tutto",
/* ja */ "すべて置換",
/* ko */ "모두 바꾸기",
/* pt_br */ "Substituir tudo",
/* ru */ "Заменить все",
/* zh_hans */ "全部替换",
/* zh_hant */ "全部取代",
],
// SearchClose (button)
[
/* en */ "Close",
/* de */ "Schließen",
/* es */ "Cerrar",
/* fr */ "Fermer",
/* it */ "Chiudi",
/* ja */ "閉じる",
/* ko */ "닫기",
/* pt_br */ "Fechar",
/* ru */ "Закрыть",
/* zh_hans */ "关闭",
/* zh_hant */ "關閉",
],
// EncodingReopen
[
/* en */ "Reopen with encoding",
/* de */ "Mit Kodierung erneut öffnen",
/* es */ "Reabrir con codificación",
/* fr */ "Rouvrir avec un encodage différent",
/* it */ "Riapri con codifica",
/* ja */ "エンコーディングで再度開く",
/* ko */ "인코딩으로 다시 열기",
/* pt_br */ "Reabrir com codificação",
/* ru */ "Открыть снова с кодировкой",
/* zh_hans */ "使用编码重新打开",
/* zh_hant */ "使用編碼重新打開",
],
// EncodingConvert
[
/* en */ "Convert to encoding",
/* de */ "In Kodierung konvertieren",
/* es */ "Convertir a otra codificación",
/* fr */ "Convertir en encodage",
/* it */ "Converti in codifica",
/* ja */ "エンコーディングに変換",
/* ko */ "인코딩으로 변환",
/* pt_br */ "Converter para codificação",
/* ru */ "Преобразовать в кодировку",
/* zh_hans */ "转换为编码",
/* zh_hant */ "轉換為編碼",
],
// IndentationTabs
[
/* en */ "Tabs",
/* de */ "Tabs",
/* es */ "Tabulaciones",
/* fr */ "Tabulations",
/* it */ "Tabulazioni",
/* ja */ "タブ",
/* ko */ "",
/* pt_br */ "Tabulações",
/* ru */ "Табы",
/* zh_hans */ "制表符",
/* zh_hant */ "製表符",
],
// IndentationSpaces
[
/* en */ "Spaces",
/* de */ "Leerzeichen",
/* es */ "Espacios",
/* fr */ "Espaces",
/* it */ "Spazi",
/* ja */ "スペース",
/* ko */ "공백",
/* pt_br */ "Espaços",
/* ru */ "Пробелы",
/* zh_hans */ "空格",
/* zh_hant */ "空格",
],
// SaveAsDialogPathLabel
[
/* en */ "Folder:",
/* de */ "Ordner:",
/* es */ "Carpeta:",
/* fr */ "Dossier:",
/* it */ "Cartella:",
/* ja */ "フォルダ:",
/* ko */ "폴더:",
/* pt_br */ "Pasta:",
/* ru */ "Папка:",
/* zh_hans */ "文件夹:",
/* zh_hant */ "資料夾:",
],
// SaveAsDialogNameLabel
[
/* en */ "File name:",
/* de */ "Dateiname:",
/* es */ "Nombre de archivo:",
/* fr */ "Nom de fichier:",
/* it */ "Nome del file:",
/* ja */ "ファイル名:",
/* ko */ "파일 이름:",
/* pt_br */ "Nome do arquivo:",
/* ru */ "Имя файла:",
/* zh_hans */ "文件名:",
/* zh_hant */ "檔案名稱:",
],
// FileOverwriteWarning
[
/* en */ "Confirm Save As",
/* de */ "Speichern unter bestätigen",
/* es */ "Confirmar Guardar como",
/* fr */ "Confirmer Enregistrer sous",
/* it */ "Conferma Salva con nome",
/* ja */ "名前を付けて保存の確認",
/* ko */ "다른 이름으로 저장 확인",
/* pt_br */ "Confirmar Salvar como",
/* ru */ "Подтвердите «Сохранить как…»",
/* zh_hans */ "确认另存为",
/* zh_hant */ "確認另存新檔",
],
// FileOverwriteWarningDescription
[
/* en */ "File already exists. Do you want to overwrite it?",
/* de */ "Datei existiert bereits. Möchten Sie sie überschreiben?",
/* es */ "El archivo ya existe. ¿Desea sobrescribirlo?",
/* fr */ "Le fichier existe déjà. Voulez-vous lécraser?",
/* it */ "Il file esiste già. Vuoi sovrascriverlo?",
/* ja */ "ファイルは既に存在します。上書きしますか?",
/* ko */ "파일이 이미 존재합니다. 덮어쓰시겠습니까?",
/* pt_br */ "O arquivo já existe. Deseja sobrescrevê-lo?",
/* ru */ "Файл уже существует. Перезаписать?",
/* zh_hans */ "文件已存在。要覆盖它吗?",
/* zh_hant */ "檔案已存在。要覆蓋它嗎?",
],
];
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;
for l in langs {
lang = match l.as_str() {
"en" => LangId::en,
"de" => LangId::de,
"es" => LangId::es,
"fr" => LangId::fr,
"it" => LangId::it,
"ja" => LangId::ja,
"ko" => LangId::ko,
"pt-br" => LangId::pt_br,
"ru" => LangId::ru,
"zh-hant" => LangId::zh_hant,
"zh" => LangId::zh_hans,
_ => continue,
};
break;
}
unsafe {
S_LANG = lang;
}
}
pub fn loc(id: LocId) -> &'static str {
S_LANG_LUT[id as usize][unsafe { S_LANG as usize }]
}

View file

@ -0,0 +1,611 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![feature(let_chains, linked_list_cursors, os_string_truncate, string_from_utf8_lossy_owned)]
mod documents;
mod draw_editor;
mod draw_filepicker;
mod draw_menubar;
mod draw_statusbar;
mod localization;
mod state;
use std::borrow::Cow;
#[cfg(feature = "debug-latency")]
use std::fmt::Write;
use std::path::Path;
use std::{env, process};
use draw_editor::*;
use draw_filepicker::*;
use draw_menubar::*;
use draw_statusbar::*;
use edit::arena::{self, ArenaString, scratch_arena};
use edit::framebuffer::{self, IndexedColor};
use edit::helpers::{KIBI, MEBI, MetricFormatter, Rect, Size};
use edit::input::{self, kbmod, vk};
use edit::oklab::oklab_blend;
use edit::tui::*;
use edit::vt::{self, Token};
use edit::{apperr, arena_format, base64, path, sys};
use localization::*;
use state::*;
#[cfg(target_pointer_width = "32")]
const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI;
#[cfg(target_pointer_width = "64")]
const SCRATCH_ARENA_CAPACITY: usize = 512 * MEBI;
fn main() -> process::ExitCode {
if cfg!(debug_assertions) {
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
drop(RestoreModes);
drop(sys::Deinit);
hook(info);
}));
}
match run() {
Ok(()) => process::ExitCode::SUCCESS,
Err(err) => {
sys::write_stdout(&format!("{}\r\n", FormatApperr::from(err)));
process::ExitCode::FAILURE
}
}
}
fn run() -> apperr::Result<()> {
// Init `sys` first, as everything else may depend on its functionality (IO, function pointers, etc.).
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.
localization::init();
let mut state = State::new()?;
if handle_args(&mut state)? {
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.
sys::switch_modes()?;
let mut vt_parser = vt::Parser::new();
let mut input_parser = input::Parser::new();
let mut tui = Tui::new()?;
let _restore = setup_terminal(&mut tui, &mut vt_parser);
state.menubar_color_bg = oklab_blend(
tui.indexed(IndexedColor::Background),
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_fg = tui.contrasted(floater_bg);
tui.setup_modifier_translations(ModifierTranslations {
ctrl: loc(LocId::Ctrl),
alt: loc(LocId::Alt),
shift: loc(LocId::Shift),
});
tui.set_floater_default_bg(floater_bg);
tui.set_floater_default_fg(floater_fg);
tui.set_modal_default_bg(floater_bg);
tui.set_modal_default_fg(floater_fg);
sys::inject_window_size_into_stdin();
#[cfg(feature = "debug-latency")]
let mut last_latency_width = 0;
loop {
#[cfg(feature = "debug-latency")]
let time_beg;
#[cfg(feature = "debug-latency")]
let mut passes;
// Process a batch of input.
{
let scratch = scratch_arena(None);
let read_timeout = vt_parser.read_timeout().min(tui.read_timeout());
let Some(input) = sys::read_stdin(&scratch, read_timeout) else {
break;
};
#[cfg(feature = "debug-latency")]
{
time_beg = std::time::Instant::now();
passes = 0usize;
}
let vt_iter = vt_parser.parse(&input);
let mut input_iter = input_parser.parse(vt_iter);
while {
let input = input_iter.next();
let more = input.is_some();
let mut ctx = tui.create_context(input);
draw(&mut ctx, &mut state);
#[cfg(feature = "debug-latency")]
{
passes += 1;
}
more
} {}
}
// Continue rendering until the layout has settled.
// This can take >1 frame, if the input focus is tossed between different controls.
while tui.needs_settling() {
let mut ctx = tui.create_context(None);
draw(&mut ctx, &mut state);
#[cfg(feature = "debug-layout")]
{
drop(ctx);
state.buffer.buffer.copy_from_str(&tui.debug_layout());
}
#[cfg(feature = "debug-latency")]
{
passes += 1;
}
}
if state.exit {
break;
}
// Render the UI and write it to the terminal.
{
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();
}
}
if state.osc_clipboard_send_generation == tui.clipboard_generation() {
write_osc_clipboard(&mut output, &mut state, &tui);
}
#[cfg(feature = "debug-latency")]
{
// 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;
let scratch_alt = scratch_arena(Some(&scratch));
let status = arena_format!(
&scratch_alt,
"{}P {}B {:.3}μs",
passes,
output.len(),
status.as_nanos() as f64 / 1000.0
);
// "μs" is 3 bytes and 2 columns.
let cols = status.len() as i32 - 3 + 2;
// Since the status may shrink and grow, we may have to overwrite the previous one with whitespace.
let padding = (last_latency_width - cols).max(0);
// If the `output` is already very large,
// Rust may double the size during the write below.
// Let's avoid that by reserving the needed size in advance.
output.reserve_exact(128);
// To avoid moving the cursor, push and pop it onto the VT cursor stack.
_ = write!(
output,
"\x1b7\x1b[0;41;97m\x1b[1;{0}H{1:2$}{3}\x1b8",
tui.size().width - cols - padding + 1,
"",
padding as usize,
status
);
last_latency_width = cols;
}
sys::write_stdout(&output);
}
}
Ok(())
}
// Returns true if the application should exit early.
fn handle_args(state: &mut State) -> apperr::Result<bool> {
let mut cwd = env::current_dir()?;
let mut path = None;
// The best CLI argument parser in the world.
if let Some(arg) = env::args_os().nth(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 == "-" {
// We'll check for a redirected stdin no matter what, so we can just ignore "-".
} else {
let p = cwd.join(Path::new(&arg));
let p = path::normalize(&p);
if let Some(parent) = p.parent() {
cwd = parent.to_path_buf();
}
path = Some(p);
}
}
if let Some(mut file) = sys::open_stdin_if_redirected() {
let doc = state.documents.add_untitled()?;
let mut tb = doc.buffer.borrow_mut();
tb.read_file(&mut file, None)?;
tb.mark_as_dirty();
} else if let Some(path) = path {
state.documents.add_file_path(&path)?;
} else {
state.documents.add_untitled()?;
}
state.file_picker_pending_dir = DisplayablePathBuf::new(cwd);
Ok(false)
}
fn print_help() {
sys::write_stdout(concat!(
"Usage: edit [OPTIONS] [FILE]\r\n",
"Options:\r\n",
" -h, --help Print this help message\r\n",
" -v, --version Print the version number\r\n",
));
}
fn print_version() {
sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\r\n"));
}
fn draw(ctx: &mut Context, state: &mut State) {
draw_menubar(ctx, state);
draw_editor(ctx, state);
draw_statusbar(ctx, state);
if state.wants_close {
draw_handle_wants_close(ctx, state);
}
if state.wants_exit {
draw_handle_wants_exit(ctx, state);
}
if state.wants_file_picker != StateFilePicker::None {
draw_file_picker(ctx, state);
}
if state.wants_save {
draw_handle_save(ctx, state);
}
if state.wants_encoding_change != StateEncodingChange::None {
draw_dialog_encoding_change(ctx, state);
}
if state.wants_document_picker {
draw_document_picker(ctx, state);
}
if state.wants_about {
draw_dialog_about(ctx, state);
}
if state.osc_clipboard_seen_generation != ctx.clipboard_generation() {
draw_handle_clipboard_change(ctx, state);
}
if state.error_log_count != 0 {
draw_error_log(ctx, state);
}
if let Some(key) = ctx.keyboard_input() {
// Shortcuts that are not handled as part of the textarea, etc.
if key == kbmod::CTRL | vk::N {
draw_add_untitled_document(ctx, state);
} else if key == kbmod::CTRL | vk::O {
state.wants_file_picker = StateFilePicker::Open;
} else if key == kbmod::CTRL | vk::S {
state.wants_save = true;
} else if key == kbmod::CTRL_SHIFT | vk::S {
state.wants_file_picker = StateFilePicker::SaveAs;
} else if key == kbmod::CTRL | vk::W {
state.wants_close = true;
} else if key == kbmod::CTRL | vk::P {
state.wants_document_picker = true;
} else if key == kbmod::CTRL | vk::Q {
state.wants_exit = true;
} else if key == kbmod::CTRL | vk::F && state.wants_search.kind != StateSearchKind::Disabled
{
state.wants_search.kind = StateSearchKind::Search;
state.wants_search.focus = true;
} else if key == kbmod::CTRL | vk::R && state.wants_search.kind != StateSearchKind::Disabled
{
state.wants_search.kind = StateSearchKind::Replace;
state.wants_search.focus = true;
} else {
return;
}
// All of the above shortcuts happen to require a rerender.
ctx.needs_rerender();
ctx.set_input_consumed();
}
}
fn draw_handle_wants_exit(_ctx: &mut Context, state: &mut State) {
while let Some(doc) = state.documents.active() {
if doc.buffer.borrow().is_dirty() {
state.wants_close = true;
return;
}
state.documents.remove_active();
}
if state.documents.len() == 0 {
state.exit = true;
}
}
#[cold]
fn write_terminal_title(output: &mut ArenaString, filename: &str) {
output.push_str("\x1b]0;");
if !filename.is_empty() {
output.push_str(&sanitize_control_chars(filename));
output.push_str(" - ");
}
output.push_str("edit\x1b\\");
}
const LARGE_CLIPBOARD_THRESHOLD: usize = 4 * KIBI;
fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
let generation = ctx.clipboard_generation();
if state.osc_clipboard_always_send || ctx.clipboard().len() < LARGE_CLIPBOARD_THRESHOLD {
state.osc_clipboard_seen_generation = generation;
state.osc_clipboard_send_generation = generation;
return;
}
let over_limit = ctx.clipboard().len() >= SCRATCH_ARENA_CAPACITY / 4;
ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
{
ctx.block_begin("description");
ctx.attr_padding(Rect::three(1, 2, 1));
if over_limit {
ctx.label("line1", loc(LocId::LargeClipboardWarningLine1));
ctx.attr_position(Position::Center);
ctx.label("line2", loc(LocId::SuperLargeClipboardWarning));
ctx.attr_position(Position::Center);
} else {
let label2 = {
let template = loc(LocId::LargeClipboardWarningLine2);
let size = arena_format!(ctx.arena(), "{}", MetricFormatter(ctx.clipboard().len()));
let mut label =
ArenaString::with_capacity_in(template.len() + size.len(), ctx.arena());
label.push_str(template);
label.replace_once_in_place("{size}", &size);
label
};
ctx.label("line1", loc(LocId::LargeClipboardWarningLine1));
ctx.attr_position(Position::Center);
ctx.label("line2", &label2);
ctx.attr_position(Position::Center);
ctx.label("line3", loc(LocId::LargeClipboardWarningLine3));
ctx.attr_position(Position::Center);
}
ctx.block_end();
ctx.table_begin("choices");
ctx.inherit_focus();
ctx.attr_padding(Rect::three(0, 2, 1));
ctx.attr_position(Position::Center);
ctx.table_set_cell_gap(Size { width: 2, height: 0 });
{
ctx.table_next_row();
ctx.inherit_focus();
if over_limit {
if ctx.button("ok", loc(LocId::Ok)) {
state.osc_clipboard_seen_generation = generation;
}
ctx.inherit_focus();
} else {
if ctx.button("always", loc(LocId::Always)) {
state.osc_clipboard_always_send = true;
state.osc_clipboard_seen_generation = generation;
state.osc_clipboard_send_generation = generation;
}
if ctx.button("yes", loc(LocId::Yes)) {
state.osc_clipboard_seen_generation = generation;
state.osc_clipboard_send_generation = generation;
}
if ctx.clipboard().len() < 10 * LARGE_CLIPBOARD_THRESHOLD {
ctx.inherit_focus();
}
if ctx.button("no", loc(LocId::No)) {
state.osc_clipboard_seen_generation = generation;
}
if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD {
ctx.inherit_focus();
}
}
}
ctx.table_end();
}
if ctx.modal_end() {
state.osc_clipboard_seen_generation = generation;
}
}
#[cold]
fn write_osc_clipboard(output: &mut ArenaString, state: &mut State, tui: &Tui) {
let clipboard = tui.clipboard();
if !clipboard.is_empty() {
// Rust doubles the size of a string when it needs to grow it.
// If `clipboard` is *really* large, this may then double
// the size of the `output` from e.g. 100MB to 200MB. Not good.
// We can avoid that by reserving the needed size in advance.
output.reserve_exact(base64::encode_len(clipboard.len()) + 16);
output.push_str("\x1b]52;c;");
base64::encode(output, clipboard);
output.push_str("\x1b\\");
}
state.osc_clipboard_send_generation = tui.clipboard_generation().wrapping_sub(1);
}
struct RestoreModes;
impl Drop for RestoreModes {
fn drop(&mut self) {
// Same as in the beginning but in the reverse order.
// It also includes DECSCUSR 0 to reset the cursor style and DECTCEM to show the cursor.
sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l");
}
}
fn setup_terminal(tui: &mut Tui, vt_parser: &mut vt::Parser) -> RestoreModes {
sys::write_stdout(concat!(
// 1049: Alternative Screen Buffer
// I put the ASB switch in the beginning, just in case the terminal performs
// some additional state tracking beyond the modes we enable/disable.
// 1002: Cell Motion Mouse Tracking
// 1006: SGR Mouse Mode
// 2004: Bracketed Paste Mode
"\x1b[?1049h\x1b[?1002;1006;2004h",
// OSC 4 color table requests for indices 0 through 15 (base colors).
"\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?\x07",
"\x1b]4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07",
// OSC 10 and 11 queries for the current foreground and background colors.
"\x1b]10;?\x07\x1b]11;?\x07",
// CSI c reports the terminal capabilities.
// It also helps us to detect the end of the responses, because not all
// terminals support the OSC queries, but all of them support CSI c.
"\x1b[c",
));
let mut done = false;
let mut osc_buffer = String::new();
let mut indexed_colors = framebuffer::DEFAULT_THEME;
let mut color_responses = 0;
while !done {
let scratch = scratch_arena(None);
let Some(input) = sys::read_stdin(&scratch, vt_parser.read_timeout()) else {
break;
};
let mut vt_stream = vt_parser.parse(&input);
while let Some(token) = vt_stream.next() {
match token {
Token::Csi(state) if state.final_byte == 'c' => done = true,
Token::Osc { mut data, partial } => {
if partial {
osc_buffer.push_str(data);
continue;
}
if !osc_buffer.is_empty() {
osc_buffer.push_str(data);
data = &osc_buffer;
}
let mut splits = data.split_terminator(';');
let color = match splits.next().unwrap_or("") {
// The response is `4;<color>;rgb:<r>/<g>/<b>`.
"4" => match splits.next().unwrap_or("").parse::<usize>() {
Ok(val) if val < 16 => &mut indexed_colors[val],
_ => continue,
},
// The response is `10;rgb:<r>/<g>/<b>`.
"10" => &mut indexed_colors[IndexedColor::Foreground as usize],
// The response is `11;rgb:<r>/<g>/<b>`.
"11" => &mut indexed_colors[IndexedColor::Background as usize],
_ => continue,
};
let color_param = splits.next().unwrap_or("");
if !color_param.starts_with("rgb:") {
continue;
}
let mut iter = color_param[4..].split_terminator('/');
let rgb_parts = [(); 3].map(|_| iter.next().unwrap_or("0"));
let mut rgb = 0;
for part in rgb_parts {
if part.len() == 2 || part.len() == 4 {
let Ok(mut val) = usize::from_str_radix(part, 16) else {
continue;
};
if part.len() == 4 {
// Round from 16 bits to 8 bits.
val = (val * 0xff + 0x7fff) / 0xffff;
}
rgb = (rgb >> 8) | ((val as u32) << 16);
}
}
*color = rgb | 0xff000000;
color_responses += 1;
osc_buffer.clear();
}
_ => {}
}
}
}
if color_responses == indexed_colors.len() {
tui.setup_indexed_colors(indexed_colors);
}
RestoreModes
}
/// Strips all C0 control characters from the string an replaces them with "_".
///
/// Jury is still out on whether this should also strip C1 control characters.
/// That requires parsing UTF8 codepoints, which is annoying.
fn sanitize_control_chars(text: &str) -> Cow<'_, str> {
if let Some(off) = text.bytes().position(|b| (..0x20).contains(&b)) {
let mut sanitized = text.to_string();
// SAFETY: We only search for ASCII and replace it with ASCII.
let vec = unsafe { sanitized.as_bytes_mut() };
for i in &mut vec[off..] {
*i = if (..0x20).contains(i) { b'_' } else { *i }
}
Cow::Owned(sanitized)
} else {
Cow::Borrowed(text)
}
}

View file

@ -0,0 +1,254 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::mem;
use std::path::{Path, PathBuf};
use edit::buffer::TextBuffer;
use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::tui::*;
use edit::{apperr, buffer, icu, sys};
use crate::documents::DocumentManager;
use crate::localization::*;
#[repr(transparent)]
pub struct FormatApperr(apperr::Error);
impl From<apperr::Error> for FormatApperr {
fn from(err: apperr::Error) -> Self {
FormatApperr(err)
}
}
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),
}
}
}
pub struct DisplayablePathBuf {
value: PathBuf,
str: Cow<'static, str>,
}
impl DisplayablePathBuf {
pub fn new(value: PathBuf) -> Self {
let str = value.to_string_lossy();
let str = unsafe { mem::transmute::<Cow<'_, str>, Cow<'_, str>>(str) };
Self { value, str }
}
pub fn as_path(&self) -> &Path {
&self.value
}
pub fn as_str(&self) -> &str {
&self.str
}
pub fn as_bytes(&self) -> &[u8] {
self.value.as_os_str().as_encoded_bytes()
}
}
impl Default for DisplayablePathBuf {
fn default() -> Self {
Self { value: Default::default(), str: Cow::Borrowed("") }
}
}
impl Clone for DisplayablePathBuf {
fn clone(&self) -> Self {
DisplayablePathBuf::new(self.value.clone())
}
}
impl From<OsString> for DisplayablePathBuf {
fn from(s: OsString) -> DisplayablePathBuf {
DisplayablePathBuf::new(PathBuf::from(s))
}
}
impl<T: ?Sized + AsRef<OsStr>> From<&T> for DisplayablePathBuf {
fn from(s: &T) -> DisplayablePathBuf {
DisplayablePathBuf::new(PathBuf::from(s))
}
}
pub struct StateSearch {
pub kind: StateSearchKind,
pub focus: bool,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum StateSearchKind {
Hidden,
Disabled,
Search,
Replace,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum StateFilePicker {
None,
Open,
SaveAs,
SaveAsShown, // Transitioned from SaveAs
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum StateEncodingChange {
None,
Convert,
Reopen,
}
pub struct State {
pub menubar_color_bg: u32,
pub menubar_color_fg: u32,
pub documents: DocumentManager,
// A ring buffer of the last 10 errors.
pub error_log: [String; 10],
pub error_log_index: usize,
pub error_log_count: usize,
pub wants_file_picker: StateFilePicker,
pub file_picker_pending_dir: DisplayablePathBuf,
pub file_picker_pending_name: PathBuf,
pub file_picker_entries: Option<Vec<DisplayablePathBuf>>,
pub file_picker_overwrite_warning: Option<PathBuf>, // The path the warning is about.
pub wants_search: StateSearch,
pub search_needle: String,
pub search_replacement: String,
pub search_options: buffer::SearchOptions,
pub search_success: bool,
pub wants_save: bool,
pub wants_statusbar_focus: bool,
pub wants_encoding_picker: bool,
pub wants_encoding_change: StateEncodingChange,
pub wants_indentation_picker: bool,
pub wants_document_picker: bool,
pub wants_about: bool,
pub wants_close: bool,
pub wants_exit: bool,
pub osc_title_filename: String,
pub osc_clipboard_seen_generation: u32,
pub osc_clipboard_send_generation: u32,
pub osc_clipboard_always_send: bool,
pub exit: bool,
}
impl State {
pub fn new() -> apperr::Result<Self> {
let buffer = TextBuffer::new_rc(false)?;
{
let mut tb = buffer.borrow_mut();
tb.set_margin_enabled(true);
tb.set_line_highlight_enabled(true);
}
Ok(Self {
menubar_color_bg: 0,
menubar_color_fg: 0,
documents: Default::default(),
error_log: [const { String::new() }; 10],
error_log_index: 0,
error_log_count: 0,
wants_file_picker: StateFilePicker::None,
file_picker_pending_dir: Default::default(),
file_picker_pending_name: Default::default(),
file_picker_entries: None,
file_picker_overwrite_warning: None,
wants_search: StateSearch { kind: StateSearchKind::Hidden, focus: false },
search_needle: Default::default(),
search_replacement: Default::default(),
search_options: Default::default(),
search_success: true,
wants_save: false,
wants_statusbar_focus: false,
wants_encoding_picker: false,
wants_encoding_change: StateEncodingChange::None,
wants_indentation_picker: false,
wants_document_picker: false,
wants_about: false,
wants_close: false,
wants_exit: false,
osc_title_filename: Default::default(),
osc_clipboard_seen_generation: 0,
osc_clipboard_send_generation: 0,
osc_clipboard_always_send: false,
exit: false,
})
}
}
pub fn draw_add_untitled_document(ctx: &mut Context, state: &mut State) {
if let Err(err) = state.documents.add_untitled() {
error_log_add(ctx, state, err);
}
}
pub fn error_log_add(ctx: &mut Context, state: &mut State, err: apperr::Error) {
let msg = format!("{}", FormatApperr::from(err));
if !msg.is_empty() {
state.error_log[state.error_log_index] = msg;
state.error_log_index = (state.error_log_index + 1) % state.error_log.len();
state.error_log_count = state.error_log.len().min(state.error_log_count + 1);
ctx.needs_rerender();
}
}
pub fn draw_error_log(ctx: &mut Context, state: &mut State) {
ctx.modal_begin("error", loc(LocId::ErrorDialogTitle));
ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
{
ctx.block_begin("content");
ctx.attr_padding(Rect::three(0, 2, 1));
{
let off = state.error_log_index + state.error_log.len() - state.error_log_count;
for i in 0..state.error_log_count {
let idx = (off + i) % state.error_log.len();
let msg = &state.error_log[idx][..];
if !msg.is_empty() {
ctx.next_block_id_mixin(i as u64);
ctx.label("error", msg);
ctx.attr_overflow(Overflow::TruncateTail);
}
}
}
ctx.block_end();
if ctx.button("ok", loc(LocId::Ok)) {
state.error_log_count = 0;
}
ctx.attr_position(Position::Center);
ctx.inherit_focus();
}
if ctx.modal_end() {
state.error_log_count = 0;
}
}

View file

@ -0,0 +1,377 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ops::Range;
use std::ptr::{self, NonNull};
use std::slice;
use crate::document::{ReadableDocument, WriteableDocument};
use crate::helpers::*;
use crate::{apperr, sys};
#[cfg(target_pointer_width = "32")]
const LARGE_CAPACITY: usize = 128 * MEBI;
#[cfg(target_pointer_width = "64")]
const LARGE_CAPACITY: usize = 4 * GIBI;
const LARGE_ALLOC_CHUNK: usize = 64 * KIBI;
const LARGE_GAP_CHUNK: usize = 4 * KIBI;
const SMALL_CAPACITY: usize = 128 * KIBI;
const SMALL_ALLOC_CHUNK: usize = 256;
const SMALL_GAP_CHUNK: usize = 16;
// TODO: Instead of having a specialization for small buffers here,
// tui.rs could also just keep a MRU set of large buffers around.
enum BackingBuffer {
VirtualMemory(NonNull<u8>, usize),
Vec(Vec<u8>),
}
impl Drop for BackingBuffer {
fn drop(&mut self) {
unsafe {
if let BackingBuffer::VirtualMemory(ptr, reserve) = *self {
sys::virtual_release(ptr, reserve);
}
}
}
}
/// Most people know how Vec<T> works: It has some spare capacity at the end,
/// so that pushing into it doesn't reallocate every single time. A gap buffer
/// is the same thing, but the spare capacity can be anywhere in the buffer.
/// This variant is optimized for large buffers and uses virtual memory.
pub struct GapBuffer {
/// Pointer to the buffer.
text: NonNull<u8>,
/// Maximum size of the buffer, including gap.
reserve: usize,
/// Size of the buffer, including gap.
commit: usize,
/// Length of the stored text, NOT including gap.
text_length: usize,
/// Gap offset.
gap_off: usize,
/// Gap length.
gap_len: usize,
/// Increments every time the buffer is modified.
generation: u32,
/// If `Vec(..)`, the buffer is optimized for small amounts of text
/// and uses the standard heap. Otherwise, it uses virtual memory.
buffer: BackingBuffer,
}
impl GapBuffer {
pub fn new(small: bool) -> apperr::Result<Self> {
let reserve;
let buffer;
let text;
if small {
reserve = SMALL_CAPACITY;
text = NonNull::dangling();
buffer = BackingBuffer::Vec(Vec::new());
} else {
reserve = LARGE_CAPACITY;
text = unsafe { sys::virtual_reserve(reserve)? };
buffer = BackingBuffer::VirtualMemory(text, reserve);
}
Ok(Self {
text,
reserve,
commit: 0,
text_length: 0,
gap_off: 0,
gap_len: 0,
generation: 0,
buffer,
})
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.text_length
}
pub fn generation(&self) -> u32 {
self.generation
}
pub fn set_generation(&mut self, generation: u32) {
self.generation = generation;
}
/// WARNING: The returned slice must not necessarily be the same length as `len` (due to OOM).
pub fn allocate_gap(&mut self, off: usize, len: usize, delete: usize) -> &mut [u8] {
// Sanitize parameters
let off = off.min(self.text_length);
let delete = delete.min(self.text_length - off);
// Move the existing gap if it exists
if off != self.gap_off {
self.move_gap(off);
}
// Delete the text
if delete > 0 {
self.delete_text(delete);
}
// Enlarge the gap if needed
if len > self.gap_len {
self.enlarge_gap(len);
}
self.generation = self.generation.wrapping_add(1);
unsafe { slice::from_raw_parts_mut(self.text.add(self.gap_off).as_ptr(), self.gap_len) }
}
fn move_gap(&mut self, off: usize) {
if self.gap_len > 0 {
//
// v gap_off
// left: |ABCDEFGHIJKLMN OPQRSTUVWXYZ|
// |ABCDEFGHI JKLMNOPQRSTUVWXYZ|
// ^ off
// move: JKLMN
//
// v gap_off
// !left: |ABCDEFGHIJKLMN OPQRSTUVWXYZ|
// |ABCDEFGHIJKLMNOPQRS TUVWXYZ|
// ^ off
// move: OPQRS
//
let left = off < self.gap_off;
let move_src = if left { off } else { self.gap_off + self.gap_len };
let move_dst = if left { off + self.gap_len } else { self.gap_off };
let move_len = if left { self.gap_off - off } else { off - self.gap_off };
unsafe { self.text.add(move_src).copy_to(self.text.add(move_dst), move_len) };
if cfg!(debug_assertions) {
// Fill the moved-out bytes with 0xCD to make debugging easier.
unsafe { self.text.add(off).write_bytes(0xCD, self.gap_len) };
}
}
self.gap_off = off;
}
fn delete_text(&mut self, delete: usize) {
if cfg!(debug_assertions) {
// Fill the deleted bytes with 0xCD to make debugging easier.
unsafe { self.text.add(self.gap_off + self.gap_len).write_bytes(0xCD, delete) };
}
self.gap_len += delete;
self.text_length -= delete;
}
fn enlarge_gap(&mut self, len: usize) {
let gap_chunk;
let alloc_chunk;
if matches!(self.buffer, BackingBuffer::VirtualMemory(..)) {
gap_chunk = LARGE_GAP_CHUNK;
alloc_chunk = LARGE_ALLOC_CHUNK;
} else {
gap_chunk = SMALL_GAP_CHUNK;
alloc_chunk = SMALL_ALLOC_CHUNK;
}
let gap_len_old = self.gap_len;
let gap_len_new = (len + gap_chunk + gap_chunk - 1) & !(gap_chunk - 1);
let bytes_old = self.commit;
let bytes_new = self.text_length + gap_len_new;
if bytes_new > bytes_old {
let bytes_new = (bytes_new + alloc_chunk - 1) & !(alloc_chunk - 1);
if bytes_new > self.reserve {
return;
}
match &mut self.buffer {
BackingBuffer::VirtualMemory(ptr, _) => unsafe {
if sys::virtual_commit(ptr.add(bytes_old), bytes_new - bytes_old).is_err() {
return;
}
},
BackingBuffer::Vec(v) => {
v.resize(bytes_new, 0);
self.text = unsafe { NonNull::new_unchecked(v.as_mut_ptr()) };
}
}
self.commit = bytes_new;
}
let gap_beg = unsafe { self.text.add(self.gap_off) };
unsafe {
ptr::copy(
gap_beg.add(gap_len_old).as_ptr(),
gap_beg.add(gap_len_new).as_ptr(),
self.text_length - self.gap_off,
)
};
if cfg!(debug_assertions) {
// Fill the moved-out bytes with 0xCD to make debugging easier.
unsafe { gap_beg.add(gap_len_old).write_bytes(0xCD, gap_len_new - gap_len_old) };
}
self.gap_len = gap_len_new;
}
pub fn commit_gap(&mut self, len: usize) {
assert!(len <= self.gap_len);
self.text_length += len;
self.gap_off += len;
self.gap_len -= len;
}
pub fn replace(&mut self, range: Range<usize>, src: &[u8]) {
let gap = self.allocate_gap(range.start, src.len(), range.end.saturating_sub(range.start));
let len = slice_copy_safe(gap, src);
self.commit_gap(len);
}
pub fn clear(&mut self) {
self.gap_off = 0;
self.gap_len += self.text_length;
self.generation = self.generation.wrapping_add(1);
self.text_length = 0;
}
pub fn extract_raw(
&self,
mut beg: usize,
mut end: usize,
out: &mut Vec<u8>,
mut out_off: usize,
) {
debug_assert!(beg <= end && end <= self.text_length);
end = end.min(self.text_length);
beg = beg.min(end);
out_off = out_off.min(out.len());
if beg >= end {
return;
}
out.reserve(end - beg);
while beg < end {
let chunk = self.read_forward(beg);
let chunk = &chunk[..chunk.len().min(end - beg)];
out.replace_range(out_off..out_off, chunk);
beg += chunk.len();
out_off += chunk.len();
}
}
/// Replaces the entire buffer contents with the given `text`.
/// The method is optimized for the case where the given `text` already matches
/// the existing contents. Returns `true` if the buffer contents were changed.
pub fn copy_from(&mut self, src: &dyn ReadableDocument) -> bool {
let mut off = 0;
// Find the position at which the contents change.
loop {
let dst_chunk = self.read_forward(off);
let src_chunk = src.read_forward(off);
let dst_len = dst_chunk.len();
let src_len = src_chunk.len();
let len = dst_len.min(src_len);
let mismatch = dst_chunk[..len] != src_chunk[..len];
if mismatch {
break; // The contents differ.
}
if len == 0 {
if dst_len == src_len {
return false; // Both done simultaneously. -> Done.
}
break; // One of the two is shorter.
}
off += len;
}
// Update the buffer starting at `off`.
loop {
let chunk = src.read_forward(off);
self.replace(off..usize::MAX, chunk);
off += chunk.len();
// No more data to copy -> Done. By checking this _after_ the replace()
// call, we ensure that the initial `off..usize::MAX` range is deleted.
// This fixes going from some buffer contents to being empty.
if chunk.is_empty() {
return true;
}
}
}
/// Copies the contents of the buffer into a string.
pub fn copy_into(&self, dst: &mut dyn WriteableDocument) {
let mut beg = 0;
let mut off = 0;
while {
let chunk = self.read_forward(off);
// The first write will be 0..usize::MAX and effectively clear() the destination.
// Every subsequent write will be usize::MAX..usize::MAX and thus effectively append().
dst.replace(beg..usize::MAX, chunk);
beg = usize::MAX;
off += chunk.len();
off < self.text_length
} {}
}
}
impl ReadableDocument for GapBuffer {
fn read_forward(&self, off: usize) -> &[u8] {
let off = off.min(self.text_length);
let beg;
let len;
if off < self.gap_off {
// Cursor is before the gap: We can read until the start of the gap.
beg = off;
len = self.gap_off - off;
} else {
// Cursor is after the gap: We can read until the end of the buffer.
beg = off + self.gap_len;
len = self.text_length - off;
}
unsafe { slice::from_raw_parts(self.text.add(beg).as_ptr(), len) }
}
fn read_backward(&self, off: usize) -> &[u8] {
let off = off.min(self.text_length);
let beg;
let len;
if off <= self.gap_off {
// Cursor is before the gap: We can read until the beginning of the buffer.
beg = 0;
len = off;
} else {
// Cursor is after the gap: We can read until the end of the gap.
beg = self.gap_off + self.gap_len;
// The cursor_off doesn't account of the gap_len.
// (This allows us to move the gap without recalculating the cursor position.)
len = off - self.gap_off;
}
unsafe { slice::from_raw_parts(self.text.add(beg).as_ptr(), len) }
}
}

2418
pkgs/edit/src/buffer/mod.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,290 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ops::Range;
use crate::document::ReadableDocument;
#[derive(Clone, Copy, PartialEq, Eq)]
enum CharClass {
Whitespace,
Newline,
Separator,
Word,
}
const fn construct_classifier(seperators: &[u8]) -> [CharClass; 256] {
let mut classifier = [CharClass::Word; 256];
classifier[b' ' as usize] = CharClass::Whitespace;
classifier[b'\t' as usize] = CharClass::Whitespace;
classifier[b'\n' as usize] = CharClass::Newline;
classifier[b'\r' as usize] = CharClass::Newline;
let mut i = 0;
let len = seperators.len();
while i < len {
let ch = seperators[i];
assert!(ch < 128, "Only ASCII separators are supported.");
classifier[ch as usize] = CharClass::Separator;
i += 1;
}
classifier
}
const WORD_CLASSIFIER: [CharClass; 256] =
construct_classifier(br#"`~!@#$%^&*()-=+[{]}\|;:'",.<>/?"#);
/// Finds the next word boundary given a document cursor offset.
/// Returns the offset of the next word boundary.
pub fn word_forward(doc: &dyn ReadableDocument, offset: usize) -> usize {
word_navigation(WordForward { doc, offset, chunk: &[], chunk_off: 0 })
}
/// The backward version of `word_forward`.
pub fn word_backward(doc: &dyn ReadableDocument, offset: usize) -> usize {
word_navigation(WordBackward { doc, offset, chunk: &[], chunk_off: 0 })
}
/// Word navigation implementation. Matches the behavior of VS Code.
fn word_navigation<T: WordNavigation>(mut nav: T) -> usize {
// First, fill `self.chunk` with at least 1 grapheme.
nav.read();
// Skip one newline, if any.
nav.skip_newline();
// Skip any whitespace.
nav.skip_class(CharClass::Whitespace);
// Skip one word or seperator and take note of the class.
let class = nav.peek(CharClass::Whitespace);
if matches!(class, CharClass::Separator | CharClass::Word) {
nav.next();
let off = nav.offset();
// Continue skipping the same class.
nav.skip_class(class);
// If the class was a separator and we only moved one character,
// continue skipping characters of the word class.
if off == nav.offset() && class == CharClass::Separator {
nav.skip_class(CharClass::Word);
}
}
nav.offset()
}
trait WordNavigation {
fn read(&mut self);
fn skip_newline(&mut self);
fn skip_class(&mut self, class: CharClass);
fn peek(&self, default: CharClass) -> CharClass;
fn next(&mut self);
fn offset(&self) -> usize;
}
struct WordForward<'a> {
doc: &'a dyn ReadableDocument,
offset: usize,
chunk: &'a [u8],
chunk_off: usize,
}
impl WordNavigation for WordForward<'_> {
fn read(&mut self) {
self.chunk = self.doc.read_forward(self.offset);
self.chunk_off = 0;
}
fn skip_newline(&mut self) {
// We can rely on the fact that the document does not split graphemes across chunks.
// = If there's a newline it's wholly contained in this chunk.
// Unlike with `WordBackward`, we can't check for CR and LF separately as only a CR followed
// by a LF is a newline. A lone CR in the document is just a regular control character.
self.chunk_off += match self.chunk.get(self.chunk_off) {
Some(&b'\n') => 1,
Some(&b'\r') if self.chunk.get(self.chunk_off + 1) == Some(&b'\n') => 2,
_ => 0,
}
}
fn skip_class(&mut self, class: CharClass) {
while !self.chunk.is_empty() {
while self.chunk_off < self.chunk.len() {
if WORD_CLASSIFIER[self.chunk[self.chunk_off] as usize] != class {
return;
}
self.chunk_off += 1;
}
self.offset += self.chunk.len();
self.chunk = self.doc.read_forward(self.offset);
self.chunk_off = 0;
}
}
fn peek(&self, default: CharClass) -> CharClass {
if self.chunk_off < self.chunk.len() {
WORD_CLASSIFIER[self.chunk[self.chunk_off] as usize]
} else {
default
}
}
fn next(&mut self) {
self.chunk_off += 1;
}
fn offset(&self) -> usize {
self.offset + self.chunk_off
}
}
struct WordBackward<'a> {
doc: &'a dyn ReadableDocument,
offset: usize,
chunk: &'a [u8],
chunk_off: usize,
}
impl WordNavigation for WordBackward<'_> {
fn read(&mut self) {
self.chunk = self.doc.read_backward(self.offset);
self.chunk_off = self.chunk.len();
}
fn skip_newline(&mut self) {
// We can rely on the fact that the document does not split graphemes across chunks.
// = If there's a newline it's wholly contained in this chunk.
if self.chunk_off > 0 && self.chunk[self.chunk_off - 1] == b'\n' {
self.chunk_off -= 1;
}
if self.chunk_off > 0 && self.chunk[self.chunk_off - 1] == b'\r' {
self.chunk_off -= 1;
}
}
fn skip_class(&mut self, class: CharClass) {
while !self.chunk.is_empty() {
while self.chunk_off > 0 {
if WORD_CLASSIFIER[self.chunk[self.chunk_off - 1] as usize] != class {
return;
}
self.chunk_off -= 1;
}
self.offset -= self.chunk.len();
self.chunk = self.doc.read_backward(self.offset);
self.chunk_off = self.chunk.len();
}
}
fn peek(&self, default: CharClass) -> CharClass {
if self.chunk_off > 0 {
WORD_CLASSIFIER[self.chunk[self.chunk_off - 1] as usize]
} else {
default
}
}
fn next(&mut self) {
self.chunk_off -= 1;
}
fn offset(&self) -> usize {
self.offset - self.chunk.len() + self.chunk_off
}
}
/// Returns the offset range of the "word" at the given offset.
/// Does not cross newlines. Works similar to VS Code.
pub fn word_select(doc: &dyn ReadableDocument, offset: usize) -> Range<usize> {
let mut beg = offset;
let mut end = offset;
let mut class = CharClass::Newline;
let mut chunk = doc.read_forward(end);
if !chunk.is_empty() {
// Not at the end of the document? Great!
// We default to using the next char as the class, because in terminals
// the cursor is usually always to the left of the cell you clicked on.
class = WORD_CLASSIFIER[chunk[0] as usize];
let mut chunk_off = 0;
// Select the word, unless we hit a newline.
if class != CharClass::Newline {
loop {
chunk_off += 1;
end += 1;
if chunk_off >= chunk.len() {
chunk = doc.read_forward(end);
chunk_off = 0;
if chunk.is_empty() {
break;
}
}
if WORD_CLASSIFIER[chunk[chunk_off] as usize] != class {
break;
}
}
}
}
let mut chunk = doc.read_backward(beg);
if !chunk.is_empty() {
let mut chunk_off = chunk.len();
// If we failed to determine the class, because we hit the end of the document
// or a newline, we fall back to using the previous character, of course.
if class == CharClass::Newline {
class = WORD_CLASSIFIER[chunk[chunk_off - 1] as usize];
}
// Select the word, unless we hit a newline.
if class != CharClass::Newline {
loop {
if WORD_CLASSIFIER[chunk[chunk_off - 1] as usize] != class {
break;
}
chunk_off -= 1;
beg -= 1;
if chunk_off == 0 {
chunk = doc.read_backward(beg);
chunk_off = chunk.len();
if chunk.is_empty() {
break;
}
}
}
}
}
beg..end
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_word_navigation() {
assert_eq!(word_forward(&"Hello World".as_bytes(), 0), 5);
assert_eq!(word_forward(&"Hello,World".as_bytes(), 0), 5);
assert_eq!(word_forward(&" Hello".as_bytes(), 0), 8);
assert_eq!(word_forward(&"\n\nHello".as_bytes(), 0), 1);
assert_eq!(word_backward(&"Hello World".as_bytes(), 11), 6);
assert_eq!(word_backward(&"Hello,World".as_bytes(), 10), 6);
assert_eq!(word_backward(&"Hello ".as_bytes(), 7), 0);
assert_eq!(word_backward(&"Hello\n\n".as_bytes(), 7), 6);
}
}

84
pkgs/edit/src/cell.rs Normal file
View file

@ -0,0 +1,84 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! [`std::cell::RefCell`], but without runtime checks in release builds.
#[cfg(debug_assertions)]
pub use debug::*;
#[cfg(not(debug_assertions))]
pub use release::*;
#[allow(unused)]
#[cfg(debug_assertions)]
mod debug {
pub type SemiRefCell<T> = std::cell::RefCell<T>;
pub type Ref<'b, T> = std::cell::Ref<'b, T>;
pub type RefMut<'b, T> = std::cell::RefMut<'b, T>;
}
#[cfg(not(debug_assertions))]
mod release {
#[derive(Default)]
#[repr(transparent)]
pub struct SemiRefCell<T>(std::cell::UnsafeCell<T>);
impl<T> SemiRefCell<T> {
#[inline(always)]
pub const fn new(value: T) -> Self {
Self(std::cell::UnsafeCell::new(value))
}
#[inline(always)]
pub const fn as_ptr(&self) -> *mut T {
self.0.get()
}
#[inline(always)]
pub const fn borrow(&self) -> Ref<'_, T> {
Ref(unsafe { &*self.0.get() })
}
#[inline(always)]
pub const fn borrow_mut(&self) -> RefMut<'_, T> {
RefMut(unsafe { &mut *self.0.get() })
}
}
#[repr(transparent)]
pub struct Ref<'b, T>(&'b T);
impl<'b, T> Ref<'b, T> {
#[inline(always)]
pub fn clone(orig: &Ref<'b, T>) -> Ref<'b, T> {
Ref(orig.0)
}
}
impl<'b, T> std::ops::Deref for Ref<'b, T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &Self::Target {
self.0
}
}
#[repr(transparent)]
pub struct RefMut<'b, T>(&'b mut T);
impl<'b, T> std::ops::Deref for RefMut<'b, T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &Self::Target {
self.0
}
}
impl<'b, T> std::ops::DerefMut for RefMut<'b, T> {
#[inline(always)]
fn deref_mut(&mut self) -> &mut Self::Target {
self.0
}
}
}

109
pkgs/edit/src/document.rs Normal file
View file

@ -0,0 +1,109 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Abstractions over reading/writing arbitrary text containers.
use std::ffi::OsString;
use std::mem;
use std::ops::Range;
use std::path::PathBuf;
use crate::arena::{ArenaString, scratch_arena};
use crate::helpers::ReplaceRange as _;
/// An abstraction over reading from text containers.
pub trait ReadableDocument {
/// Read some bytes starting at (including) the given absolute offset.
///
/// # Warning
///
/// * Be lenient on inputs:
/// * The given offset may be out of bounds and you MUST clamp it.
/// * You should not assume that offsets are at grapheme cluster boundaries.
/// * Be strict on outputs:
/// * You MUST NOT break grapheme clusters across chunks.
/// * You MUST NOT return an empty slice unless the offset is at or beyond the end.
fn read_forward(&self, off: usize) -> &[u8];
/// Read some bytes before (but not including) the given absolute offset.
///
/// # Warning
///
/// * Be lenient on inputs:
/// * The given offset may be out of bounds and you MUST clamp it.
/// * You should not assume that offsets are at grapheme cluster boundaries.
/// * Be strict on outputs:
/// * You MUST NOT break grapheme clusters across chunks.
/// * You MUST NOT return an empty slice unless the offset is zero.
fn read_backward(&self, off: usize) -> &[u8];
}
/// An abstraction over writing to text containers.
pub trait WriteableDocument: ReadableDocument {
/// Replace the given range with the given bytes.
///
/// # Warning
///
/// * The given range may be out of bounds and you MUST clamp it.
/// * The replacement may not be valid UTF8.
fn replace(&mut self, range: Range<usize>, replacement: &[u8]);
}
impl ReadableDocument for &[u8] {
fn read_forward(&self, off: usize) -> &[u8] {
let s = *self;
&s[off.min(s.len())..]
}
fn read_backward(&self, off: usize) -> &[u8] {
let s = *self;
&s[..off.min(s.len())]
}
}
impl ReadableDocument for String {
fn read_forward(&self, off: usize) -> &[u8] {
let s = self.as_bytes();
&s[off.min(s.len())..]
}
fn read_backward(&self, off: usize) -> &[u8] {
let s = self.as_bytes();
&s[..off.min(s.len())]
}
}
impl WriteableDocument for String {
fn replace(&mut self, range: Range<usize>, replacement: &[u8]) {
// `replacement` is not guaranteed to be valid UTF-8, so we need to sanitize it.
let scratch = scratch_arena(None);
let utf8 = ArenaString::from_utf8_lossy(&scratch, replacement);
let src = match &utf8 {
Ok(s) => s,
Err(s) => s.as_str(),
};
// SAFETY: `range` is guaranteed to be on codepoint boundaries.
unsafe { self.as_mut_vec() }.replace_range(range, src.as_bytes());
}
}
impl ReadableDocument for PathBuf {
fn read_forward(&self, off: usize) -> &[u8] {
let s = self.as_os_str().as_encoded_bytes();
&s[off.min(s.len())..]
}
fn read_backward(&self, off: usize) -> &[u8] {
let s = self.as_os_str().as_encoded_bytes();
&s[..off.min(s.len())]
}
}
impl WriteableDocument for PathBuf {
fn replace(&mut self, range: Range<usize>, replacement: &[u8]) {
let mut vec = mem::take(self).into_os_string().into_encoded_bytes();
vec.replace_range(range, replacement);
*self = unsafe { PathBuf::from(OsString::from_encoded_bytes_unchecked(vec)) };
}
}

View file

@ -0,0 +1,888 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! A shoddy framebuffer for terminal applications.
use std::cell::Cell;
use std::fmt::Write;
use std::ops::{BitOr, BitXor};
use std::ptr;
use std::slice::ChunksExact;
use crate::arena::{Arena, ArenaString};
use crate::helpers::{CoordType, Point, Rect, Size};
use crate::oklab::{oklab_blend, srgb_to_oklab};
use crate::simd::{MemsetSafe, memset};
use crate::unicode::MeasurementConfig;
// Same constants as used in the PCG family of RNGs.
#[cfg(target_pointer_width = "32")]
const HASH_MULTIPLIER: usize = 747796405; // https://doi.org/10.1090/S0025-5718-99-00996-5, Table 5
#[cfg(target_pointer_width = "64")]
const HASH_MULTIPLIER: usize = 6364136223846793005; // Knuth's MMIX multiplier
/// The size of our cache table. 1<<8 = 256.
const CACHE_TABLE_LOG2_SIZE: usize = 8;
const CACHE_TABLE_SIZE: usize = 1 << CACHE_TABLE_LOG2_SIZE;
/// To index into the cache table, we use `color * HASH_MULTIPLIER` as the hash.
/// Since the multiplication "shifts" the bits up, we don't just mask the lowest
/// 8 bits out, but rather shift 56 bits down to get the best bits from the top.
const CACHE_TABLE_SHIFT: usize = usize::BITS as usize - CACHE_TABLE_LOG2_SIZE;
/// Standard 16 VT & default foreground/background colors.
#[derive(Clone, Copy)]
pub enum IndexedColor {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
BrightBlack,
BrightRed,
BrightGreen,
BrightYellow,
BrightBlue,
BrightMagenta,
BrightCyan,
BrightWhite,
Background,
Foreground,
}
/// 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, 0xff212cbe, 0xff3aae3f, 0xff4a9abe, 0xffbe4d20, 0xffbe54bb, 0xffb2a700, 0xffbebebe,
0xff808080, 0xff303eff, 0xff51ea58, 0xff44c9ff, 0xffff6a2f, 0xffff74fc, 0xfff0e100, 0xffffffff,
0xff000000, 0xffffffff,
];
/// A shoddy framebuffer for terminal applications.
///
/// The idea is that you create a [`Framebuffer`], draw a bunch of text and
/// colors into it, and it takes care of figuring out what changed since the
/// last rendering and sending the differences as VT to the terminal.
///
/// This is an improvement over how many other terminal applications work,
/// as they fail to accurately track what changed. If you watch the output
/// of `vim` for instance, you'll notice that it redraws unrelated parts of
/// the screen all the time.
pub struct Framebuffer {
/// Store the color palette.
indexed_colors: [u32; INDEXED_COLORS_COUNT],
/// Front and back buffers. Indexed by `frame_counter & 1`.
buffers: [Buffer; 2],
/// The current frame counter. Increments on every `flip` call.
frame_counter: usize,
/// 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],
/// 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,
}
impl Framebuffer {
/// Creates a new framebuffer.
pub fn new() -> Self {
Self {
indexed_colors: DEFAULT_THEME,
buffers: Default::default(),
frame_counter: 0,
auto_colors: [
DEFAULT_THEME[IndexedColor::Black as usize],
DEFAULT_THEME[IndexedColor::White as usize],
],
contrast_colors: [const { Cell::new((0, 0)) }; CACHE_TABLE_SIZE],
background_fill: DEFAULT_THEME[IndexedColor::Background as usize],
foreground_fill: DEFAULT_THEME[IndexedColor::Foreground as usize],
}
}
/// Sets the base color palette.
///
/// 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]) {
self.indexed_colors = colors;
self.background_fill = 0;
self.foreground_fill = 0;
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]) {
self.auto_colors.swap(0, 1);
}
}
/// Begins a new frame with the given `size`.
pub fn flip(&mut self, size: Size) {
if size != self.buffers[0].bg_bitmap.size {
for buffer in &mut self.buffers {
buffer.text = LineBuffer::new(size);
buffer.bg_bitmap = Bitmap::new(size);
buffer.fg_bitmap = Bitmap::new(size);
buffer.attributes = AttributeBuffer::new(size);
}
let front = &mut self.buffers[self.frame_counter & 1];
// Trigger a full redraw. (Yes, it's a hack.)
front.fg_bitmap.fill(1);
// Trigger a cursor update as well, just to be sure.
front.cursor = Cursor::new_invalid();
}
self.frame_counter = self.frame_counter.wrapping_add(1);
let back = &mut self.buffers[self.frame_counter & 1];
back.text.fill_whitespace();
back.bg_bitmap.fill(self.background_fill);
back.fg_bitmap.fill(self.foreground_fill);
back.attributes.reset();
back.cursor = Cursor::new_disabled();
}
/// Replaces text contents in a single line of the framebuffer.
/// All coordinates are in viewport coordinates.
/// Assumes that control characters have been replaced or escaped.
pub fn replace_text(
&mut self,
y: CoordType,
origin_x: CoordType,
clip_right: CoordType,
text: &str,
) {
let back = &mut self.buffers[self.frame_counter & 1];
back.text.replace_text(y, origin_x, clip_right, text)
}
/// Draws a scrollbar in the given `track` rectangle.
///
/// Not entirely sure why I put it here instead of elsewhere.
///
/// # Parameters
///
/// * `clip_rect`: Clips the rendering to this rectangle.
/// This is relevant when you have scrollareas inside scrollareas.
/// * `track`: The rectangle in which to draw the scrollbar.
/// In absolute viewport coordinates.
/// * `content_offset`: The current offset of the scrollarea.
/// * `content_height`: The height of the scrollarea content.
pub fn draw_scrollbar(
&mut self,
clip_rect: Rect,
track: Rect,
content_offset: CoordType,
content_height: CoordType,
) -> CoordType {
let track_clipped = track.intersect(clip_rect);
if track_clipped.is_empty() {
return 0;
}
let viewport_height = track.height();
// The content height is at least the viewport height.
let content_height = content_height.max(viewport_height);
// No need to draw a scrollbar if the content fits in the viewport.
let content_offset_max = content_height - viewport_height;
if content_offset_max == 0 {
return 0;
}
// The content offset must be at least one viewport height from the bottom.
// You don't want to scroll past the end after all...
let content_offset = content_offset.clamp(0, content_offset_max);
// In order to increase the visual resolution of the scrollbar,
// we'll use 1/8th blocks to represent the thumb.
// First, scale the offsets to get that 1/8th resolution.
let viewport_height = viewport_height as i64 * 8;
let content_offset_max = content_offset_max as i64 * 8;
let content_offset = content_offset as i64 * 8;
let content_height = content_height as i64 * 8;
// The proportional thumb height (0-1) is the fraction of viewport and
// content height. The taller the content, the smaller the thumb:
// = viewport_height / content_height
// We then scale that to the viewport height to get the height in 1/8th units.
// = viewport_height * viewport_height / content_height
// We add content_height/2 to round the integer division, which results in a numerator of:
// = viewport_height * viewport_height + content_height / 2
let numerator = viewport_height * viewport_height + content_height / 2;
let thumb_height = numerator / content_height;
// Ensure the thumb has a minimum size of 1 row.
let thumb_height = thumb_height.max(8);
// The proportional thumb top position (0-1) is:
// = content_offset / content_offset_max
// The maximum thumb top position is the viewport height minus the thumb height:
// = viewport_height - thumb_height
// To get the thumb top position in 1/8th units, we multiply both:
// = (viewport_height - thumb_height) * content_offset / content_offset_max
// We add content_offset_max/2 to round the integer division, which results in a numerator of:
// = (viewport_height - thumb_height) * content_offset + content_offset_max / 2
let numerator = (viewport_height - thumb_height) * content_offset + content_offset_max / 2;
let thumb_top = numerator / content_offset_max;
// The thumb bottom position is the thumb top position plus the thumb height.
let thumb_bottom = thumb_top + thumb_height;
// Shift to absolute coordinates.
let thumb_top = thumb_top + track.top as i64 * 8;
let thumb_bottom = thumb_bottom + track.top as i64 * 8;
// Clamp to the visible area.
let thumb_top = thumb_top.max(track_clipped.top as i64 * 8);
let thumb_bottom = thumb_bottom.min(track_clipped.bottom as i64 * 8);
// Calculate the height of the top/bottom cell of the thumb.
let top_fract = (thumb_top % 8) as CoordType;
let bottom_fract = (thumb_bottom % 8) as CoordType;
// Shift to absolute coordinates.
let thumb_top = ((thumb_top + 7) / 8) as CoordType;
let thumb_bottom = (thumb_bottom / 8) as CoordType;
self.blend_bg(track_clipped, self.indexed(IndexedColor::BrightBlack));
self.blend_fg(track_clipped, self.indexed(IndexedColor::BrightWhite));
// Draw the full blocks.
for y in thumb_top..thumb_bottom {
self.replace_text(y, track_clipped.left, track_clipped.right, "");
}
// Draw the top/bottom cell of the thumb.
// U+2581 to U+2588, 1/8th block to 8/8th block elements glyphs: ▁▂▃▄▅▆▇█
// In UTF8: E2 96 81 to E2 96 88
let mut fract_buf = [0xE2, 0x96, 0x88];
if top_fract != 0 {
fract_buf[2] = (0x88 - top_fract) as u8;
self.replace_text(thumb_top - 1, track_clipped.left, track_clipped.right, unsafe {
std::str::from_utf8_unchecked(&fract_buf)
});
}
if bottom_fract != 0 {
fract_buf[2] = (0x88 - bottom_fract) as u8;
self.replace_text(thumb_bottom, track_clipped.left, track_clipped.right, unsafe {
std::str::from_utf8_unchecked(&fract_buf)
});
let rect = Rect {
left: track_clipped.left,
top: thumb_bottom,
right: track_clipped.right,
bottom: thumb_bottom + 1,
};
self.blend_bg(rect, self.indexed(IndexedColor::BrightWhite));
self.blend_fg(rect, self.indexed(IndexedColor::BrightBlack));
}
((thumb_height + 4) / 8) as CoordType
}
#[inline]
pub fn indexed(&self, index: IndexedColor) -> u32 {
self.indexed_colors[index as usize]
}
/// Returns a color from the palette.
///
/// 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];
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
}
/// 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;
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];
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) {
let back = &mut self.buffers[self.frame_counter & 1];
back.bg_bitmap.blend(target, bg);
}
/// Blends the given sRGB color onto the foreground bitmap.
///
/// 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) {
let back = &mut self.buffers[self.frame_counter & 1];
back.fg_bitmap.blend(target, fg);
}
/// Reverses the foreground and background colors in the given rectangle.
pub fn reverse(&mut self, target: Rect) {
let back = &mut self.buffers[self.frame_counter & 1];
let target = target.intersect(back.bg_bitmap.size.as_rect());
if target.is_empty() {
return;
}
let top = target.top as usize;
let bottom = target.bottom as usize;
let left = target.left as usize;
let right = target.right as usize;
let stride = back.bg_bitmap.size.width as usize;
for y in top..bottom {
let beg = y * stride + left;
let end = y * stride + right;
let bg = &mut back.bg_bitmap.data[beg..end];
let fg = &mut back.fg_bitmap.data[beg..end];
bg.swap_with_slice(fg);
}
}
/// Replaces VT attributes in the given rectangle.
pub fn replace_attr(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
let back = &mut self.buffers[self.frame_counter & 1];
back.attributes.replace(target, mask, attr);
}
/// Sets the current visible cursor position and type.
///
/// Call this when focus is inside an editable area and you want to show the cursor.
pub fn set_cursor(&mut self, pos: Point, overtype: bool) {
let back = &mut self.buffers[self.frame_counter & 1];
back.cursor.pos = pos;
back.cursor.overtype = overtype;
}
/// Renders the framebuffer contents accumulated since the
/// last call to `flip()` and returns them serialized as VT.
pub fn render<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> {
let idx = self.frame_counter & 1;
// Borrows the front/back buffers without letting Rust know that we have a reference to self.
// SAFETY: Well this is certainly correct, but whether Rust and its strict rules likes it is another question.
let (back, front) = unsafe {
let ptr = self.buffers.as_mut_ptr();
let back = &mut *ptr.add(idx);
let front = &*ptr.add(1 - idx);
(back, front)
};
let mut front_lines = front.text.lines.iter(); // hahaha
let mut front_bgs = front.bg_bitmap.iter();
let mut front_fgs = front.fg_bitmap.iter();
let mut front_attrs = front.attributes.iter();
let mut back_lines = back.text.lines.iter();
let mut back_bgs = back.bg_bitmap.iter();
let mut back_fgs = back.fg_bitmap.iter();
let mut back_attrs = back.attributes.iter();
let mut result = ArenaString::new_in(arena);
let mut last_bg = u64::MAX;
let mut last_fg = u64::MAX;
let mut last_attr = Attributes::None;
for y in 0..front.text.size.height {
// SAFETY: The only thing that changes the size of these containers,
// is the reset() method and it always resets front/back to the same size.
let front_line = unsafe { front_lines.next().unwrap_unchecked() };
let front_bg = unsafe { front_bgs.next().unwrap_unchecked() };
let front_fg = unsafe { front_fgs.next().unwrap_unchecked() };
let front_attr = unsafe { front_attrs.next().unwrap_unchecked() };
let back_line = unsafe { back_lines.next().unwrap_unchecked() };
let back_bg = unsafe { back_bgs.next().unwrap_unchecked() };
let back_fg = unsafe { back_fgs.next().unwrap_unchecked() };
let back_attr = unsafe { back_attrs.next().unwrap_unchecked() };
// TODO: Ideally, we should properly diff the contents and so if
// only parts of a line change, we should only update those parts.
if front_line == back_line
&& front_bg == back_bg
&& front_fg == back_fg
&& front_attr == back_attr
{
continue;
}
let line_bytes = back_line.as_bytes();
let mut cfg = MeasurementConfig::new(&line_bytes);
let mut chunk_end = 0;
if result.is_empty() {
result.push_str("\x1b[m");
}
_ = write!(result, "\x1b[{};1H", y + 1);
while {
let bg = back_bg[chunk_end];
let fg = back_fg[chunk_end];
let attr = back_attr[chunk_end];
// Chunk into runs of the same color.
while {
chunk_end += 1;
chunk_end < back_bg.len()
&& back_bg[chunk_end] == bg
&& back_fg[chunk_end] == fg
&& back_attr[chunk_end] == attr
} {}
if last_bg != bg as u64 {
last_bg = bg as u64;
self.format_color(&mut result, false, bg);
}
if last_fg != fg as u64 {
last_fg = fg as u64;
self.format_color(&mut result, true, fg);
}
if last_attr != attr {
let diff = last_attr ^ attr;
if diff.is(Attributes::Italic) {
if attr.is(Attributes::Italic) {
result.push_str("\x1b[3m");
} else {
result.push_str("\x1b[23m");
}
}
if diff.is(Attributes::Underlined) {
if attr.is(Attributes::Underlined) {
result.push_str("\x1b[4m");
} else {
result.push_str("\x1b[24m");
}
}
last_attr = attr;
}
let beg = cfg.cursor().offset;
let end = cfg.goto_visual(Point { x: chunk_end as CoordType, y: 0 }).offset;
result.push_str(&back_line[beg..end]);
chunk_end < back_bg.len()
} {}
}
// If the cursor has changed since the last frame we naturally need to update it,
// but this also applies if the code above wrote to the screen,
// as it uses CUP sequences to reposition the cursor for writing.
if !result.is_empty() || back.cursor != front.cursor {
if back.cursor.pos.x >= 0 && back.cursor.pos.y >= 0 {
// CUP to the cursor position.
// DECSCUSR to set the cursor style.
// DECTCEM to show the cursor.
_ = write!(
result,
"\x1b[{};{}H\x1b[{} q\x1b[?25h",
back.cursor.pos.y + 1,
back.cursor.pos.x + 1,
if back.cursor.overtype { 1 } else { 5 }
);
} else {
// DECTCEM to hide the cursor.
result.push_str("\x1b[?25l");
}
}
result
}
fn format_color(&self, dst: &mut ArenaString, fg: bool, mut color: u32) {
let typ = if fg { '3' } else { '4' };
// Some terminals support transparent backgrounds which are used
// if the default background color is active (CSI 49 m).
//
// If [`Framebuffer::set_indexed_colors`] was never called, we assume
// that the terminal doesn't support transparency and initialize the
// background bitmap with the `DEFAULT_THEME` default background color.
// Otherwise, we assume that the terminal supports transparency
// and initialize it with 0x00000000 (transparent).
//
// We also apply this to the foreground color, because it compresses
// 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 {
_ = write!(dst, "\x1b[{typ}9m");
return;
}
if (color & 0xff000000) != 0xff000000 {
let idx = if fg { IndexedColor::Foreground } else { IndexedColor::Background };
let dst = self.indexed(idx);
color = oklab_blend(dst, color);
}
let r = color & 0xff;
let g = (color >> 8) & 0xff;
let b = (color >> 16) & 0xff;
_ = write!(dst, "\x1b[{typ}8;2;{r};{g};{b}m");
}
}
#[derive(Default)]
struct Buffer {
text: LineBuffer,
bg_bitmap: Bitmap,
fg_bitmap: Bitmap,
attributes: AttributeBuffer,
cursor: Cursor,
}
/// A buffer for the text contents of the framebuffer.
#[derive(Default)]
struct LineBuffer {
lines: Vec<String>,
size: Size,
}
impl LineBuffer {
fn new(size: Size) -> Self {
Self { lines: vec![String::new(); size.height as usize], size }
}
fn fill_whitespace(&mut self) {
let width = self.size.width as usize;
for l in &mut self.lines {
l.clear();
l.reserve(width + width / 2);
let buf = unsafe { l.as_mut_vec() };
// Compiles down to `memset()`.
buf.extend(std::iter::repeat_n(b' ', width));
}
}
/// Replaces text contents in a single line of the framebuffer.
/// All coordinates are in viewport coordinates.
/// Assumes that control characters have been replaced or escaped.
fn replace_text(
&mut self,
y: CoordType,
origin_x: CoordType,
clip_right: CoordType,
text: &str,
) {
let Some(line) = self.lines.get_mut(y as usize) else {
return;
};
let bytes = text.as_bytes();
let clip_right = clip_right.clamp(0, self.size.width);
let layout_width = clip_right - origin_x;
// Can't insert text that can't fit or is empty.
if layout_width <= 0 || bytes.is_empty() {
return;
}
let mut cfg = MeasurementConfig::new(&bytes);
// Check if the text intersects with the left edge of the framebuffer
// and figure out the parts that are inside.
let mut left = origin_x;
if left < 0 {
let mut cursor = cfg.goto_visual(Point { x: -left, y: 0 });
if left + cursor.visual_pos.x < 0 && cursor.offset < text.len() {
// `-left` must've intersected a wide glyph and since goto_visual stops _before_ reaching the target,
// we stoped before the wide glyph and thus must step forward to the next glyph.
cursor = cfg.goto_logical(Point { x: cursor.logical_pos.x + 1, y: 0 });
}
left += cursor.visual_pos.x;
}
// If the text still starts outside the framebuffer, we must've ran out of text above.
// Otherwise, if it starts outside the right edge to begin with, we can't insert it anyway.
if left < 0 || left >= clip_right {
return;
}
// Measure the width of the new text (= `res_new.visual_target.x`).
let beg_off = cfg.cursor().offset;
let end = cfg.goto_visual(Point { x: layout_width, y: 0 });
// Figure out at which byte offset the new text gets inserted.
let right = left + end.visual_pos.x;
let line_bytes = line.as_bytes();
let mut cfg_old = MeasurementConfig::new(&line_bytes);
let res_old_beg = cfg_old.goto_visual(Point { x: left, y: 0 });
let mut res_old_end = cfg_old.goto_visual(Point { x: right, y: 0 });
// Since the goto functions will always stop short of the target position,
// we need to manually step beyond it if we intersect with a wide glyph.
if res_old_end.visual_pos.x < right {
res_old_end = cfg_old.goto_logical(Point { x: res_old_end.logical_pos.x + 1, y: 0 });
}
// If we intersect a wide glyph, we need to pad the new text with spaces.
let src = &text[beg_off..end.offset];
let overlap_beg = (left - res_old_beg.visual_pos.x).max(0) as usize;
let overlap_end = (res_old_end.visual_pos.x - right).max(0) as usize;
let total_add = src.len() + overlap_beg + overlap_end;
let total_del = res_old_end.offset - res_old_beg.offset;
// This is basically a hand-written version of `Vec::splice()`,
// but for strings under the assumption that all inputs are valid.
// It also takes care of `overlap_beg` and `overlap_end` by inserting spaces.
unsafe {
// SAFETY: Our ucd code only returns valid UTF-8 offsets.
// If it didn't that'd be a priority -9000 bug for any text editor.
// And apart from that, all inputs are &str (= UTF8).
let dst = line.as_mut_vec();
let dst_len = dst.len();
let src_len = src.len();
// Make room for the new elements. NOTE that this must be done before
// we call as_mut_ptr, or else we risk accessing a stale pointer.
// We only need to reserve as much as the string actually grows by.
dst.reserve(total_add.saturating_sub(total_del));
// Move the pointer to the start of the insert.
let mut ptr = dst.as_mut_ptr().add(res_old_beg.offset);
// Move the tail end of the string by `total_add - total_del`-many bytes.
// This both effectively deletes the old text and makes room for the new text.
if total_add != total_del {
// Move the tail of the vector to make room for the new elements.
ptr::copy(
ptr.add(total_del),
ptr.add(total_add),
dst_len - total_del - res_old_beg.offset,
);
}
// Pad left.
for _ in 0..overlap_beg {
ptr.write(b' ');
ptr = ptr.add(1);
}
// Copy the new elements into the vector.
ptr::copy_nonoverlapping(src.as_ptr(), ptr, src_len);
ptr = ptr.add(src_len);
// Pad right.
for _ in 0..overlap_end {
ptr.write(b' ');
ptr = ptr.add(1);
}
// Update the length of the vector.
dst.set_len(dst_len - total_del + total_add);
}
}
}
/// An sRGB bitmap.
#[derive(Default)]
struct Bitmap {
data: Vec<u32>,
size: Size,
}
impl Bitmap {
fn new(size: Size) -> Self {
Self { data: vec![0; (size.width * size.height) as usize], size }
}
fn fill(&mut self, color: u32) {
memset(&mut self.data, color);
}
/// Blends the given sRGB color onto the 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 {
return;
}
let target = target.intersect(self.size.as_rect());
if target.is_empty() {
return;
}
let top = target.top as usize;
let bottom = target.bottom as usize;
let left = target.left as usize;
let right = target.right as usize;
let stride = self.size.width as usize;
for y in top..bottom {
let beg = y * stride + left;
let end = y * stride + right;
let data = &mut self.data[beg..end];
if (color & 0xff000000) == 0xff000000 {
memset(data, color);
} else {
let end = data.len();
let mut off = 0;
while {
let c = data[off];
// Chunk into runs of the same color, so that we only call alpha_blend once per run.
let chunk_beg = off;
while {
off += 1;
off < end && data[off] == c
} {}
let chunk_end = off;
let c = oklab_blend(c, color);
memset(&mut data[chunk_beg..chunk_end], c);
off < end
} {}
}
}
}
/// Iterates over each row in the bitmap.
fn iter(&self) -> ChunksExact<u32> {
self.data.chunks_exact(self.size.width as usize)
}
}
/// A bitfield for VT text attributes.
///
/// It being a bitfield allows for simple diffing.
#[repr(transparent)]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Attributes(u8);
#[allow(non_upper_case_globals)]
impl Attributes {
pub const None: Attributes = Attributes(0);
pub const Italic: Attributes = Attributes(0b1);
pub const Underlined: Attributes = Attributes(0b10);
pub const All: Attributes = Attributes(0b11);
pub const fn is(self, attr: Attributes) -> bool {
(self.0 & attr.0) == attr.0
}
}
unsafe impl MemsetSafe for Attributes {}
impl BitOr for Attributes {
type Output = Attributes;
fn bitor(self, rhs: Self) -> Self::Output {
Attributes(self.0 | rhs.0)
}
}
impl BitXor for Attributes {
type Output = Attributes;
fn bitxor(self, rhs: Self) -> Self::Output {
Attributes(self.0 ^ rhs.0)
}
}
/// Stores VT attributes for the framebuffer.
#[derive(Default)]
struct AttributeBuffer {
data: Vec<Attributes>,
size: Size,
}
impl AttributeBuffer {
fn new(size: Size) -> Self {
Self { data: vec![Default::default(); (size.width * size.height) as usize], size }
}
fn reset(&mut self) {
memset(&mut self.data, Default::default());
}
fn replace(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
let target = target.intersect(self.size.as_rect());
if target.is_empty() {
return;
}
let top = target.top as usize;
let bottom = target.bottom as usize;
let left = target.left as usize;
let right = target.right as usize;
let stride = self.size.width as usize;
for y in top..bottom {
let beg = y * stride + left;
let end = y * stride + right;
let dst = &mut self.data[beg..end];
if mask == Attributes::All {
memset(dst, attr);
} else {
for a in dst {
*a = Attributes(a.0 & !mask.0 | attr.0);
}
}
}
}
/// Iterates over each row in the bitmap.
fn iter(&self) -> ChunksExact<Attributes> {
self.data.chunks_exact(self.size.width as usize)
}
}
/// Stores cursor position and type for the framebuffer.
#[derive(Default, PartialEq, Eq)]
struct Cursor {
pos: Point,
overtype: bool,
}
impl Cursor {
const fn new_invalid() -> Self {
Self { pos: Point::MIN, overtype: false }
}
const fn new_disabled() -> Self {
Self { pos: Point { x: -1, y: -1 }, overtype: false }
}
}

221
pkgs/edit/src/fuzzy.rs Normal file
View file

@ -0,0 +1,221 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Fuzzy search algorithm based on the one used in VS Code (`/src/vs/base/common/fuzzyScorer.ts`).
//! Other algorithms exist, such as Sublime Text's, or the one used in `fzf`,
//! but I figured that this one is what lots of people may be familiar with.
use std::vec;
use crate::arena::{Arena, scratch_arena};
use crate::icu;
const NO_MATCH: i32 = 0;
pub fn score_fuzzy<'a>(
arena: &'a Arena,
haystack: &str,
needle: &str,
allow_non_contiguous_matches: bool,
) -> (i32, Vec<usize, &'a Arena>) {
if haystack.is_empty() || needle.is_empty() {
// return early if target or query are empty
return (NO_MATCH, Vec::new_in(arena));
}
let scratch = scratch_arena(Some(arena));
let target = map_chars(&scratch, haystack);
let query = map_chars(&scratch, needle);
if target.len() < query.len() {
// impossible for query to be contained in target
return (NO_MATCH, Vec::new_in(arena));
}
let target_lower = icu::fold_case(&scratch, haystack);
let query_lower = icu::fold_case(&scratch, needle);
let target_lower = map_chars(&scratch, &target_lower);
let query_lower = map_chars(&scratch, &query_lower);
let area = query.len() * target.len();
let mut scores = vec::from_elem_in(0, area, &*scratch);
let mut matches = vec::from_elem_in(0, area, &*scratch);
//
// Build Scorer Matrix:
//
// The matrix is composed of query q and target t. For each index we score
// q[i] with t[i] and compare that with the previous score. If the score is
// equal or larger, we keep the match. In addition to the score, we also keep
// the length of the consecutive matches to use as boost for the score.
//
// t a r g e t
// q
// u
// e
// r
// y
//
for query_index in 0..query.len() {
let query_index_offset = query_index * target.len();
let query_index_previous_offset =
if query_index > 0 { (query_index - 1) * target.len() } else { 0 };
for target_index in 0..target.len() {
let current_index = query_index_offset + target_index;
let diag_index = if query_index > 0 && target_index > 0 {
query_index_previous_offset + target_index - 1
} else {
0
};
let left_score = if target_index > 0 { scores[current_index - 1] } else { 0 };
let diag_score =
if query_index > 0 && target_index > 0 { scores[diag_index] } else { 0 };
let matches_sequence_len =
if query_index > 0 && target_index > 0 { matches[diag_index] } else { 0 };
// If we are not matching on the first query character any more, we only produce a
// score if we had a score previously for the last query index (by looking at the diagScore).
// This makes sure that the query always matches in sequence on the target. For example
// given a target of "ede" and a query of "de", we would otherwise produce a wrong high score
// for query[1] ("e") matching on target[0] ("e") because of the "beginning of word" boost.
let score = if diag_score == 0 && query_index != 0 {
0
} else {
compute_char_score(
query[query_index],
query_lower[query_index],
if target_index != 0 { Some(target[target_index - 1]) } else { None },
target[target_index],
target_lower[target_index],
matches_sequence_len,
)
};
// We have a score and its equal or larger than the left score
// Match: sequence continues growing from previous diag value
// Score: increases by diag score value
let is_valid_score = score != 0 && diag_score + score >= left_score;
if is_valid_score
&& (
// We don't need to check if it's contiguous if we allow non-contiguous matches
allow_non_contiguous_matches ||
// We must be looking for a contiguous match.
// Looking at an index higher than 0 in the query means we must have already
// found out this is contiguous otherwise there wouldn't have been a score
query_index > 0 ||
// lastly check if the query is completely contiguous at this index in the target
target_lower[target_index..].starts_with(&query_lower)
)
{
matches[current_index] = matches_sequence_len + 1;
scores[current_index] = diag_score + score;
} else {
// We either have no score or the score is lower than the left score
// Match: reset to 0
// Score: pick up from left hand side
matches[current_index] = NO_MATCH;
scores[current_index] = left_score;
}
}
}
// Restore Positions (starting from bottom right of matrix)
let mut positions = Vec::new_in(arena);
if !query.is_empty() && !target.is_empty() {
let mut query_index = query.len() - 1;
let mut target_index = target.len() - 1;
loop {
let current_index = query_index * target.len() + target_index;
if matches[current_index] == NO_MATCH {
if target_index == 0 {
break;
}
target_index -= 1; // go left
} else {
positions.push(target_index);
// go up and left
if query_index == 0 || target_index == 0 {
break;
}
query_index -= 1;
target_index -= 1;
}
}
positions.reverse();
}
(scores[area - 1], positions)
}
fn compute_char_score(
query: char,
query_lower: char,
target_prev: Option<char>,
target_curr: char,
target_curr_lower: char,
matches_sequence_len: i32,
) -> i32 {
let mut score = 0;
if !consider_as_equal(query_lower, target_curr_lower) {
return score; // no match of characters
}
// Character match bonus
score += 1;
// Consecutive match bonus
if matches_sequence_len > 0 {
score += matches_sequence_len * 5;
}
// Same case bonus
if query == target_curr {
score += 1;
}
if let Some(target_prev) = target_prev {
// After separator bonus
let separator_bonus = score_separator_at_pos(target_prev);
if separator_bonus > 0 {
score += separator_bonus;
}
// Inside word upper case bonus (camel case). We only give this bonus if we're not in a contiguous sequence.
// For example:
// NPE => NullPointerException = boost
// HTTP => HTTP = not boost
else if target_curr != target_curr_lower && matches_sequence_len == 0 {
score += 2;
}
} else {
// Start of word bonus
score += 8;
}
score
}
fn consider_as_equal(a: char, b: char) -> bool {
// Special case path separators: ignore platform differences
a == b || a == '/' || a == '\\' && b == '/' || b == '\\'
}
fn score_separator_at_pos(ch: char) -> i32 {
match ch {
'/' | '\\' => 5, // prefer path separators...
'_' | '-' | '.' | ' ' | '\'' | '"' | ':' => 4, // ...over other separators
_ => 0,
}
}
fn map_chars<'a>(arena: &'a Arena, s: &str) -> Vec<char, &'a Arena> {
let mut chars = Vec::with_capacity_in(s.len(), arena);
chars.extend(s.chars());
chars.shrink_to_fit();
chars
}

93
pkgs/edit/src/hash.rs Normal file
View file

@ -0,0 +1,93 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Provides fast, non-cryptographic hash functions.
/// The venerable wyhash hash function.
///
/// It's fast, has good statistical properties, and is in the public domain.
/// See: <https://github.com/wangyi-fudan/wyhash>
/// If you visit the link, you'll find that it was superseded by "rapidhash",
/// but that's not particularly interesting for this project. rapidhash results
/// in way larger assembly and isn't faster when hashing small amounts of data.
pub fn hash(mut seed: u64, data: &[u8]) -> u64 {
unsafe {
const S0: u64 = 0xa0761d6478bd642f;
const S1: u64 = 0xe7037ed1a0b428db;
const S2: u64 = 0x8ebc6af09c88c6e3;
const S3: u64 = 0x589965cc75374cc3;
let len = data.len();
let mut p = data.as_ptr();
let a;
let b;
seed ^= S0;
if len <= 16 {
if len >= 4 {
a = (wyr4(p) << 32) | wyr4(p.add((len >> 3) << 2));
b = (wyr4(p.add(len - 4)) << 32) | wyr4(p.add(len - 4 - ((len >> 3) << 2)));
} else if len > 0 {
a = wyr3(p, len);
b = 0;
} else {
a = 0;
b = 0;
}
} else {
let mut i = len;
if i > 48 {
let mut seed1 = seed;
let mut seed2 = seed;
while {
seed = wymix(wyr8(p) ^ S1, wyr8(p.add(8)) ^ seed);
seed1 = wymix(wyr8(p.add(16)) ^ S2, wyr8(p.add(24)) ^ seed1);
seed2 = wymix(wyr8(p.add(32)) ^ S3, wyr8(p.add(40)) ^ seed2);
p = p.add(48);
i -= 48;
i > 48
} {}
seed ^= seed1 ^ seed2;
}
while i > 16 {
seed = wymix(wyr8(p) ^ S1, wyr8(p.add(8)) ^ seed);
i -= 16;
p = p.add(16);
}
a = wyr8(p.offset(i as isize - 16));
b = wyr8(p.offset(i as isize - 8));
}
wymix(S1 ^ (len as u64), wymix(a ^ S1, b ^ seed))
}
}
unsafe fn wyr3(p: *const u8, k: usize) -> u64 {
let p0 = unsafe { p.read() as u64 };
let p1 = unsafe { p.add(k >> 1).read() as u64 };
let p2 = unsafe { p.add(k - 1).read() as u64 };
(p0 << 16) | (p1 << 8) | p2
}
unsafe fn wyr4(p: *const u8) -> u64 {
unsafe { (p as *const u32).read_unaligned() as u64 }
}
unsafe fn wyr8(p: *const u8) -> u64 {
unsafe { (p as *const u64).read_unaligned() }
}
// This is a weak mix function on its own. It may be worth considering
// replacing external uses of this function with a stronger one.
// On the other hand, it's very fast.
pub fn wymix(lhs: u64, rhs: u64) -> u64 {
let lhs = lhs as u128;
let rhs = rhs as u128;
let r = lhs * rhs;
(r >> 64) as u64 ^ (r as u64)
}
pub fn hash_str(seed: u64, s: &str) -> u64 {
hash(seed, s.as_bytes())
}

277
pkgs/edit/src/helpers.rs Normal file
View file

@ -0,0 +1,277 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! 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;
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 = i32;
/// To avoid overflow issues because you're adding two [`CoordType::MAX`] values together,
/// you can use [`COORD_TYPE_SAFE_MIN`] and [`COORD_TYPE_SAFE_MAX`].
pub const COORD_TYPE_SAFE_MAX: CoordType = 32767;
/// See [`COORD_TYPE_SAFE_MAX`].
pub const COORD_TYPE_SAFE_MIN: CoordType = -32767 - 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: Point = Point { x: CoordType::MIN, y: CoordType::MIN };
pub const MAX: Point = Point { x: CoordType::MAX, y: CoordType::MAX };
}
impl PartialOrd<Point> for Point {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Point {
fn cmp(&self, other: &Self) -> Ordering {
match self.y.cmp(&other.y) {
Ordering::Equal => self.x.cmp(&other.x),
ord => ord,
}
}
}
/// 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);
Rect { left: l, top: t, right: r, bottom: b }
}
}
/// [`std::cmp::minmax`] is unstable, as per usual.
pub fn minmax<T>(v1: T, v2: T) -> [T; 2]
where
T: Ord,
{
if v2 < v1 { [v2, v1] } else { [v1, v2] }
}
#[inline(always)]
#[allow(clippy::ptr_eq)]
fn opt_ptr<T>(a: Option<&T>) -> *const T {
unsafe { mem::transmute(a) }
}
/// Surprisingly, there's no way in Rust to do a `ptr::eq` on `Option<&T>`.
/// Uses `unsafe` so that the debug performance isn't too bad.
#[inline(always)]
#[allow(clippy::ptr_eq)]
pub fn opt_ptr_eq<T>(a: Option<&T>, b: Option<&T>) -> bool {
opt_ptr(a) == opt_ptr(b)
}
/// Creates a `&str` from a pointer and a length.
/// Exists, because `std::str::from_raw_parts` is unstable, par for the course.
///
/// # Safety
///
/// The given data must be valid UTF-8.
/// The given data must outlive the returned reference.
#[inline]
#[must_use]
pub const unsafe fn str_from_raw_parts<'a>(ptr: *const u8, len: usize) -> &'a str {
unsafe { str::from_utf8_unchecked(slice::from_raw_parts(ptr, len)) }
}
/// [`<[T]>::copy_from_slice`] panics if the two slices have different lengths.
/// This one just returns the copied amount.
pub fn slice_copy_safe<T: Copy>(dst: &mut [T], src: &[T]) -> usize {
let len = src.len().min(dst.len());
unsafe { ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), len) };
len
}
/// [`Vec::splice`] results in really bad assembly.
/// This doesn't. Don't use [`Vec::splice`].
pub trait ReplaceRange<T: Copy> {
fn replace_range<R: RangeBounds<usize>>(&mut self, range: R, src: &[T]);
}
impl<T: Copy, A: Allocator> ReplaceRange<T> for Vec<T, A> {
fn replace_range<R: RangeBounds<usize>>(&mut self, range: R, src: &[T]) {
let start = match range.start_bound() {
Bound::Included(&start) => start,
Bound::Excluded(start) => start + 1,
Bound::Unbounded => 0,
};
let end = match range.end_bound() {
Bound::Included(end) => end + 1,
Bound::Excluded(&end) => end,
Bound::Unbounded => usize::MAX,
};
vec_replace_impl(self, start..end, src);
}
}
fn vec_replace_impl<T: Copy, A: Allocator>(dst: &mut Vec<T, A>, range: Range<usize>, src: &[T]) {
unsafe {
let dst_len = dst.len();
let src_len = src.len();
let off = range.start.min(dst_len);
let del_len = range.end.saturating_sub(off).min(dst_len - off);
if del_len == 0 && src_len == 0 {
return; // nothing to do
}
let tail_len = dst_len - off - del_len;
let new_len = dst_len - del_len + src_len;
if src_len > del_len {
dst.reserve(src_len - del_len);
}
// NOTE: drop_in_place() is not needed here, because T is constrained to Copy.
// SAFETY: as_mut_ptr() must called after reserve() to ensure that the pointer is valid.
let ptr = dst.as_mut_ptr().add(off);
// Shift the tail.
if tail_len > 0 && src_len != del_len {
ptr::copy(ptr.add(del_len), ptr.add(src_len), tail_len);
}
// Copy in the replacement.
ptr::copy_nonoverlapping(src.as_ptr(), ptr, src_len);
dst.set_len(new_len);
}
}
/// [`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>] {
unsafe { slice::from_raw_parts(slice.as_ptr() as *const MaybeUninit<T>, slice.len()) }
}
/// Turns a [`&mut [T]`] into a [`&mut [MaybeUninit<T>]`].
#[inline(always)]
pub const fn slice_as_uninit_mut<T>(slice: &mut [T]) -> &mut [MaybeUninit<T>] {
unsafe { slice::from_raw_parts_mut(slice.as_mut_ptr() as *mut MaybeUninit<T>, slice.len()) }
}

1242
pkgs/edit/src/icu.rs Normal file

File diff suppressed because it is too large Load diff

577
pkgs/edit/src/input.rs Normal file
View file

@ -0,0 +1,577 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Parses VT sequences into input events.
//!
//! In the future this allows us to take apart the application and
//! support input schemes that aren't VT, such as UEFI, or GUI.
use crate::helpers::{CoordType, Point, Size};
use crate::vt;
/// Represents a key/modifier combination.
///
/// TODO: Is this a good idea? I did it to allow typing `kbmod::CTRL | vk::A`.
/// The reason it's an awkard u32 and not a struct is to hopefully make ABIs easier later.
/// Of course you could just translate on the ABI boundary, but my hope is that this
/// design lets me realize some restrictions early on that I can't foresee yet.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct InputKey(u32);
impl InputKey {
pub(crate) const fn new(v: u32) -> Self {
Self(v)
}
pub(crate) const fn from_ascii(ch: char) -> Option<Self> {
if ch == ' ' || (ch >= '0' && ch <= '9') {
Some(Self(ch as u32))
} else if ch >= 'a' && ch <= 'z' {
Some(Self(ch as u32 & !0x20)) // Shift a-z to A-Z
} else if ch >= 'A' && ch <= 'Z' {
Some(Self(kbmod::SHIFT.0 | ch as u32))
} else {
None
}
}
pub(crate) const fn value(&self) -> u32 {
self.0
}
pub(crate) const fn key(&self) -> InputKey {
InputKey(self.0 & 0x00FFFFFF)
}
pub(crate) const fn modifiers(&self) -> InputKeyMod {
InputKeyMod(self.0 & 0xFF000000)
}
pub(crate) const fn modifiers_contains(&self, modifier: InputKeyMod) -> bool {
(self.0 & modifier.0) != 0
}
pub(crate) const fn with_modifiers(&self, modifiers: InputKeyMod) -> InputKey {
InputKey(self.0 | modifiers.0)
}
}
/// A keyboard modifier. Ctrl/Alt/Shift.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct InputKeyMod(u32);
impl InputKeyMod {
const fn new(v: u32) -> Self {
Self(v)
}
pub(crate) const fn contains(&self, modifier: InputKeyMod) -> bool {
(self.0 & modifier.0) != 0
}
}
impl std::ops::BitOr<InputKeyMod> for InputKey {
type Output = InputKey;
fn bitor(self, rhs: InputKeyMod) -> InputKey {
InputKey(self.0 | rhs.0)
}
}
impl std::ops::BitOr<InputKey> for InputKeyMod {
type Output = InputKey;
fn bitor(self, rhs: InputKey) -> InputKey {
InputKey(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for InputKeyMod {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
/// Keyboard keys.
///
/// The codes defined here match the VK_* constants on Windows.
/// It's a convenient way to handle keyboard input, even on other platforms.
pub mod vk {
use super::InputKey;
pub const NULL: InputKey = InputKey::new('\0' as u32);
pub const BACK: InputKey = InputKey::new(0x08);
pub const TAB: InputKey = InputKey::new('\t' as u32);
pub const RETURN: InputKey = InputKey::new('\r' as u32);
pub const ESCAPE: InputKey = InputKey::new(0x1B);
pub const SPACE: InputKey = InputKey::new(' ' as u32);
pub const PRIOR: InputKey = InputKey::new(0x21);
pub const NEXT: InputKey = InputKey::new(0x22);
pub const END: InputKey = InputKey::new(0x23);
pub const HOME: InputKey = InputKey::new(0x24);
pub const LEFT: InputKey = InputKey::new(0x25);
pub const UP: InputKey = InputKey::new(0x26);
pub const RIGHT: InputKey = InputKey::new(0x27);
pub const DOWN: InputKey = InputKey::new(0x28);
pub const INSERT: InputKey = InputKey::new(0x2D);
pub const DELETE: InputKey = InputKey::new(0x2E);
pub const N0: InputKey = InputKey::new('0' as u32);
pub const N1: InputKey = InputKey::new('1' as u32);
pub const N2: InputKey = InputKey::new('2' as u32);
pub const N3: InputKey = InputKey::new('3' as u32);
pub const N4: InputKey = InputKey::new('4' as u32);
pub const N5: InputKey = InputKey::new('5' as u32);
pub const N6: InputKey = InputKey::new('6' as u32);
pub const N7: InputKey = InputKey::new('7' as u32);
pub const N8: InputKey = InputKey::new('8' as u32);
pub const N9: InputKey = InputKey::new('9' as u32);
pub const A: InputKey = InputKey::new('A' as u32);
pub const B: InputKey = InputKey::new('B' as u32);
pub const C: InputKey = InputKey::new('C' as u32);
pub const D: InputKey = InputKey::new('D' as u32);
pub const E: InputKey = InputKey::new('E' as u32);
pub const F: InputKey = InputKey::new('F' as u32);
pub const G: InputKey = InputKey::new('G' as u32);
pub const H: InputKey = InputKey::new('H' as u32);
pub const I: InputKey = InputKey::new('I' as u32);
pub const J: InputKey = InputKey::new('J' as u32);
pub const K: InputKey = InputKey::new('K' as u32);
pub const L: InputKey = InputKey::new('L' as u32);
pub const M: InputKey = InputKey::new('M' as u32);
pub const N: InputKey = InputKey::new('N' as u32);
pub const O: InputKey = InputKey::new('O' as u32);
pub const P: InputKey = InputKey::new('P' as u32);
pub const Q: InputKey = InputKey::new('Q' as u32);
pub const R: InputKey = InputKey::new('R' as u32);
pub const S: InputKey = InputKey::new('S' as u32);
pub const T: InputKey = InputKey::new('T' as u32);
pub const U: InputKey = InputKey::new('U' as u32);
pub const V: InputKey = InputKey::new('V' as u32);
pub const W: InputKey = InputKey::new('W' as u32);
pub const X: InputKey = InputKey::new('X' as u32);
pub const Y: InputKey = InputKey::new('Y' as u32);
pub const Z: InputKey = InputKey::new('Z' as u32);
pub const NUMPAD0: InputKey = InputKey::new(0x60);
pub const NUMPAD1: InputKey = InputKey::new(0x61);
pub const NUMPAD2: InputKey = InputKey::new(0x62);
pub const NUMPAD3: InputKey = InputKey::new(0x63);
pub const NUMPAD4: InputKey = InputKey::new(0x64);
pub const NUMPAD5: InputKey = InputKey::new(0x65);
pub const NUMPAD6: InputKey = InputKey::new(0x66);
pub const NUMPAD7: InputKey = InputKey::new(0x67);
pub const NUMPAD8: InputKey = InputKey::new(0x68);
pub const NUMPAD9: InputKey = InputKey::new(0x69);
pub const MULTIPLY: InputKey = InputKey::new(0x6A);
pub const ADD: InputKey = InputKey::new(0x6B);
pub const SEPARATOR: InputKey = InputKey::new(0x6C);
pub const SUBTRACT: InputKey = InputKey::new(0x6D);
pub const DECIMAL: InputKey = InputKey::new(0x6E);
pub const DIVIDE: InputKey = InputKey::new(0x6F);
pub const F1: InputKey = InputKey::new(0x70);
pub const F2: InputKey = InputKey::new(0x71);
pub const F3: InputKey = InputKey::new(0x72);
pub const F4: InputKey = InputKey::new(0x73);
pub const F5: InputKey = InputKey::new(0x74);
pub const F6: InputKey = InputKey::new(0x75);
pub const F7: InputKey = InputKey::new(0x76);
pub const F8: InputKey = InputKey::new(0x77);
pub const F9: InputKey = InputKey::new(0x78);
pub const F10: InputKey = InputKey::new(0x79);
pub const F11: InputKey = InputKey::new(0x7A);
pub const F12: InputKey = InputKey::new(0x7B);
pub const F13: InputKey = InputKey::new(0x7C);
pub const F14: InputKey = InputKey::new(0x7D);
pub const F15: InputKey = InputKey::new(0x7E);
pub const F16: InputKey = InputKey::new(0x7F);
pub const F17: InputKey = InputKey::new(0x80);
pub const F18: InputKey = InputKey::new(0x81);
pub const F19: InputKey = InputKey::new(0x82);
pub const F20: InputKey = InputKey::new(0x83);
pub const F21: InputKey = InputKey::new(0x84);
pub const F22: InputKey = InputKey::new(0x85);
pub const F23: InputKey = InputKey::new(0x86);
pub const F24: InputKey = InputKey::new(0x87);
}
/// Keyboard modifiers.
pub mod kbmod {
use super::InputKeyMod;
pub const NONE: InputKeyMod = InputKeyMod::new(0x00000000);
pub const CTRL: InputKeyMod = InputKeyMod::new(0x01000000);
pub const ALT: InputKeyMod = InputKeyMod::new(0x02000000);
pub const SHIFT: InputKeyMod = InputKeyMod::new(0x04000000);
pub const CTRL_ALT: InputKeyMod = InputKeyMod::new(0x03000000);
pub const CTRL_SHIFT: InputKeyMod = InputKeyMod::new(0x05000000);
pub const ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x06000000);
pub const CTRL_ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x07000000);
}
/// Text input.
///
/// "Keyboard" input is also "text" input and vice versa.
/// It differs in that text input can also be Unicode.
#[derive(Clone, Copy)]
pub struct InputText<'a> {
pub text: &'a str,
pub bracketed: bool,
}
/// Mouse input state. Up/Down, Left/Right, etc.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum InputMouseState {
#[default]
None,
// These 3 carry their state between frames.
Left,
Middle,
Right,
// These 2 get reset to None on the next frame.
Release,
Scroll,
}
/// Mouse input.
#[derive(Clone, Copy)]
pub struct InputMouse {
/// The state of the mouse.Up/Down, Left/Right, etc.
pub state: InputMouseState,
/// Any keyboard modifiers that are held down.
pub modifiers: InputKeyMod,
/// Position of the mouse in the viewport.
pub position: Point,
/// Scroll delta.
pub scroll: Point,
}
/// Primary result type of the parser.
pub enum Input<'input> {
/// Window resize event.
Resize(Size),
/// Text input.
///
/// Note that [`Input::Keyboard`] events can also be text.
Text(InputText<'input>),
/// Keyboard input.
Keyboard(InputKey),
/// Mouse input.
Mouse(InputMouse),
}
/// Parses VT sequences into input events.
pub struct Parser {
bracketed_paste: bool,
x10_mouse_want: bool,
x10_mouse_buf: [u8; 3],
x10_mouse_len: usize,
}
impl Parser {
/// Creates a new parser that turns VT sequences into input events.
///
/// Keep the instance alive for the lifetime of the input stream.
pub fn new() -> Self {
Self {
bracketed_paste: false,
x10_mouse_want: false,
x10_mouse_buf: [0; 3],
x10_mouse_len: 0,
}
}
/// Takes an [`vt::Stream`] and returns a [`Stream`]
/// that turns VT sequences into input events.
pub fn parse<'parser, 'vt, 'input>(
&'parser mut self,
stream: vt::Stream<'vt, 'input>,
) -> Stream<'parser, 'vt, 'input> {
Stream { parser: self, stream }
}
}
/// An iterator that parses VT sequences into input events.
///
/// Can't implement [`Iterator`], because this is a "lending iterator".
pub struct Stream<'parser, 'vt, 'input> {
parser: &'parser mut Parser,
stream: vt::Stream<'vt, 'input>,
}
impl<'input> Stream<'_, '_, 'input> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<Input<'input>> {
loop {
if self.parser.bracketed_paste {
return self.handle_bracketed_paste();
}
if self.parser.x10_mouse_want {
return self.parse_x10_mouse_coordinates();
}
match self.stream.next()? {
vt::Token::Text(text) => {
return Some(Input::Text(InputText { text, bracketed: false }));
}
vt::Token::Ctrl(ch) => match ch {
'\0' | '\t' | '\r' => return Some(Input::Keyboard(InputKey::new(ch as u32))),
'\n' => return Some(Input::Keyboard(kbmod::CTRL | vk::RETURN)),
..='\x1a' => {
// Shift control code to A-Z
let key = ch as u32 | 0x40;
return Some(Input::Keyboard(kbmod::CTRL | InputKey::new(key)));
}
'\x7f' => return Some(Input::Keyboard(vk::BACK)),
_ => {}
},
vt::Token::Esc(ch) => {
match ch {
'\0' => return Some(Input::Keyboard(vk::ESCAPE)),
'\n' => return Some(Input::Keyboard(kbmod::CTRL_ALT | vk::RETURN)),
' '..='~' => {
let ch = ch as u32;
let key = ch & !0x20; // Shift a-z to A-Z
let modifiers =
if (ch & 0x20) != 0 { kbmod::ALT } else { kbmod::ALT_SHIFT };
return Some(Input::Keyboard(modifiers | InputKey::new(key)));
}
_ => {}
}
}
vt::Token::SS3(ch) => {
if ('P'..='S').contains(&ch) {
let key = vk::F1.value() + ch as u32 - 'P' as u32;
return Some(Input::Keyboard(InputKey::new(key)));
}
}
vt::Token::Csi(csi) => {
match csi.final_byte {
'A'..='H' => {
const LUT: [u8; 8] = [
vk::UP.value() as u8, // A
vk::DOWN.value() as u8, // B
vk::RIGHT.value() as u8, // C
vk::LEFT.value() as u8, // D
0, // E
vk::END.value() as u8, // F
0, // G
vk::HOME.value() as u8, // H
];
let vk = LUT[csi.final_byte as usize - 'A' as usize];
if vk != 0 {
return Some(Input::Keyboard(
InputKey::new(vk as u32) | Self::parse_modifiers(csi),
));
}
}
'Z' => return Some(Input::Keyboard(kbmod::SHIFT | vk::TAB)),
'~' => {
const LUT: [u8; 35] = [
0,
vk::HOME.value() as u8, // 1
vk::INSERT.value() as u8, // 2
vk::DELETE.value() as u8, // 3
vk::END.value() as u8, // 4
vk::PRIOR.value() as u8, // 5
vk::NEXT.value() as u8, // 6
0,
0,
0,
0,
0,
0,
0,
0,
vk::F5.value() as u8, // 15
0,
vk::F6.value() as u8, // 17
vk::F7.value() as u8, // 18
vk::F8.value() as u8, // 19
vk::F9.value() as u8, // 20
vk::F10.value() as u8, // 21
0,
vk::F11.value() as u8, // 23
vk::F12.value() as u8, // 24
vk::F13.value() as u8, // 25
vk::F14.value() as u8, // 26
0,
vk::F15.value() as u8, // 28
vk::F16.value() as u8, // 29
0,
vk::F17.value() as u8, // 31
vk::F18.value() as u8, // 32
vk::F19.value() as u8, // 33
vk::F20.value() as u8, // 34
];
const LUT_LEN: u16 = LUT.len() as u16;
match csi.params[0] {
0..LUT_LEN => {
let vk = LUT[csi.params[0] as usize];
if vk != 0 {
return Some(Input::Keyboard(
InputKey::new(vk as u32) | Self::parse_modifiers(csi),
));
}
}
200 => self.parser.bracketed_paste = true,
_ => {}
}
}
'm' | 'M' if csi.private_byte == '<' => {
let btn = csi.params[0];
let mut mouse = InputMouse {
state: InputMouseState::None,
modifiers: kbmod::NONE,
position: Default::default(),
scroll: Default::default(),
};
mouse.state = InputMouseState::None;
if (btn & 0x40) != 0 {
mouse.state = InputMouseState::Scroll;
mouse.scroll.y += if (btn & 0x01) != 0 { 3 } else { -3 };
} else if csi.final_byte == 'M' {
const STATES: [InputMouseState; 4] = [
InputMouseState::Left,
InputMouseState::Middle,
InputMouseState::Right,
InputMouseState::None,
];
mouse.state = STATES[(btn as usize) & 0x03];
}
mouse.modifiers = kbmod::NONE;
mouse.modifiers |=
if (btn & 0x04) != 0 { kbmod::SHIFT } else { kbmod::NONE };
mouse.modifiers |=
if (btn & 0x08) != 0 { kbmod::ALT } else { kbmod::NONE };
mouse.modifiers |=
if (btn & 0x10f) != 0 { kbmod::CTRL } else { kbmod::NONE };
mouse.position.x = csi.params[1] as CoordType - 1;
mouse.position.y = csi.params[2] as CoordType - 1;
return Some(Input::Mouse(mouse));
}
'M' if csi.param_count == 0 => {
self.parser.x10_mouse_want = true;
}
't' if csi.params[0] == 8 => {
// Window Size
let width = (csi.params[2] as CoordType).clamp(1, 32767);
let height = (csi.params[1] as CoordType).clamp(1, 32767);
return Some(Input::Resize(Size { width, height }));
}
_ => {}
}
}
_ => {}
}
}
}
/// Once we encounter the start of a bracketed paste
/// we seek to the end of the paste in this function.
///
/// A bracketed paste is basically:
/// ```text
/// <ESC>[201~ lots of text <ESC>[201~
/// ```
///
/// That text inbetween is then expected to be taken literally.
/// It can inbetween be anything though, including other escape sequences.
/// This is the reason why this is a separate method.
#[cold]
fn handle_bracketed_paste(&mut self) -> Option<Input<'input>> {
let beg = self.stream.offset();
let mut end = beg;
while let Some(token) = self.stream.next() {
if let vt::Token::Csi(csi) = token
&& csi.final_byte == '~'
&& csi.params[0] == 201
{
self.parser.bracketed_paste = false;
break;
}
end = self.stream.offset();
}
if end != beg {
let input = self.stream.input();
Some(Input::Text(InputText { text: &input[beg..end], bracketed: true }))
} else {
None
}
}
/// Implements the X10 mouse protocol via `CSI M CbCxCy`.
///
/// You want to send numeric mouse coordinates.
/// You have CSI sequences with numeric parameters.
/// So, of course you put the coordinates as shifted ASCII characters after
/// the end of the sequence. Limited coordinate range and complicated parsing!
/// 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..]);
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 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 {
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,
};
self.parser.x10_mouse_want = false;
self.parser.x10_mouse_len = 0;
Some(Input::Mouse(InputMouse {
state: action,
modifiers,
position: Point { x, y },
scroll: Default::default(),
}))
}
fn parse_modifiers(csi: &vt::Csi) -> InputKeyMod {
let mut modifiers = kbmod::NONE;
let p1 = csi.params[1].saturating_sub(1);
if (p1 & 0x01) != 0 {
modifiers |= kbmod::SHIFT;
}
if (p1 & 0x02) != 0 {
modifiers |= kbmod::ALT;
}
if (p1 & 0x04) != 0 {
modifiers |= kbmod::CTRL;
}
modifiers
}
}

37
pkgs/edit/src/lib.rs Normal file
View file

@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#![feature(
allocator_api,
breakpoint,
cold_path,
inherent_str_constructors,
let_chains,
linked_list_cursors,
maybe_uninit_fill,
maybe_uninit_slice,
maybe_uninit_uninit_array_transpose,
os_string_truncate
)]
#![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;
pub mod document;
pub mod framebuffer;
pub mod hash;
pub mod helpers;
pub mod icu;
pub mod input;
pub mod oklab;
pub mod path;
pub mod simd;
pub mod sys;
pub mod tui;
pub mod unicode;
pub mod vt;

128
pkgs/edit/src/oklab.rs Normal file
View file

@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Oklab colorspace conversions.
//!
//! Implements Oklab as defined at: <https://bottosson.github.io/posts/oklab/>
#![allow(clippy::excessive_precision)]
/// An Oklab color with alpha.
pub struct Lab {
pub l: f32,
pub a: f32,
pub b: f32,
pub alpha: f32,
}
/// 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);
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);
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,
}
}
/// 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)
}
/// 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);
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;
oklab_to_srgb(Lab { l, a, b, alpha })
}
fn linear_to_srgb(c: f32) -> u32 {
(if c > 0.0031308 {
255.0 * 1.055 * c.powf(1.0 / 2.4) - 255.0 * 0.055
} else {
255.0 * 12.92 * c
}) as u32
}
#[inline]
fn cbrtf_est(a: f32) -> f32 {
// http://metamerist.com/cbrt/cbrt.htm showed a great estimator for the cube root:
// f32_as_uint32_t / 3 + 709921077
// It's similar to the well known "fast inverse square root" trick.
// Lots of numbers around 709921077 perform at least equally well to 709921077,
// and it is unknown how and why 709921077 was chosen specifically.
let u: u32 = f32::to_bits(a); // evil f32ing point bit level hacking
let u = u / 3 + 709921077; // what the fuck?
let x: f32 = f32::from_bits(u);
// One round of Newton's method. It follows the Wikipedia article at
// https://en.wikipedia.org/wiki/Cube_root#Numerical_methods
// For `a`s in the range between 0 and 1, this results in a maximum error of
// less than 6.7e-4f, which is not good, but good enough for us, because
// we're not an image editor. The benefit is that it's really fast.
(1.0 / 3.0) * (a / (x * x) + (x + x)) // 1st iteration
}
#[rustfmt::skip]
#[allow(clippy::excessive_precision)]
const SRGB_TO_RGB_LUT: [f32; 256] = [
0.0000000000, 0.0003035270, 0.0006070540, 0.0009105810, 0.0012141080, 0.0015176350, 0.0018211619, 0.0021246888, 0.0024282159, 0.0027317430, 0.0030352699, 0.0033465356, 0.0036765069, 0.0040247170, 0.0043914421, 0.0047769533,
0.0051815170, 0.0056053917, 0.0060488326, 0.0065120910, 0.0069954102, 0.0074990317, 0.0080231922, 0.0085681248, 0.0091340570, 0.0097212177, 0.0103298230, 0.0109600937, 0.0116122449, 0.0122864870, 0.0129830306, 0.0137020806,
0.0144438436, 0.0152085144, 0.0159962922, 0.0168073755, 0.0176419523, 0.0185002182, 0.0193823613, 0.0202885624, 0.0212190095, 0.0221738834, 0.0231533647, 0.0241576303, 0.0251868572, 0.0262412224, 0.0273208916, 0.0284260381,
0.0295568332, 0.0307134409, 0.0318960287, 0.0331047624, 0.0343398079, 0.0356013142, 0.0368894450, 0.0382043645, 0.0395462364, 0.0409151986, 0.0423114114, 0.0437350273, 0.0451862030, 0.0466650836, 0.0481718220, 0.0497065634,
0.0512694679, 0.0528606549, 0.0544802807, 0.0561284944, 0.0578054339, 0.0595112406, 0.0612460710, 0.0630100295, 0.0648032799, 0.0666259527, 0.0684781820, 0.0703601092, 0.0722718611, 0.0742135793, 0.0761853904, 0.0781874284,
0.0802198276, 0.0822827145, 0.0843762159, 0.0865004659, 0.0886556059, 0.0908417329, 0.0930589810, 0.0953074843, 0.0975873619, 0.0998987406, 0.1022417471, 0.1046164930, 0.1070231125, 0.1094617173, 0.1119324341, 0.1144353822,
0.1169706732, 0.1195384338, 0.1221387982, 0.1247718409, 0.1274376959, 0.1301364899, 0.1328683347, 0.1356333494, 0.1384316236, 0.1412633061, 0.1441284865, 0.1470272839, 0.1499598026, 0.1529261619, 0.1559264660, 0.1589608639,
0.1620294005, 0.1651322246, 0.1682693958, 0.1714410931, 0.1746473908, 0.1778884083, 0.1811642349, 0.1844749898, 0.1878207624, 0.1912016720, 0.1946178079, 0.1980693042, 0.2015562356, 0.2050787061, 0.2086368501, 0.2122307271,
0.2158605307, 0.2195262313, 0.2232279778, 0.2269658893, 0.2307400703, 0.2345506549, 0.2383976579, 0.2422811985, 0.2462013960, 0.2501583695, 0.2541521788, 0.2581829131, 0.2622507215, 0.2663556635, 0.2704978585, 0.2746773660,
0.2788943350, 0.2831487954, 0.2874408960, 0.2917706966, 0.2961383164, 0.3005438447, 0.3049873710, 0.3094689548, 0.3139887452, 0.3185468316, 0.3231432438, 0.3277781308, 0.3324515820, 0.3371636569, 0.3419144452, 0.3467040956,
0.3515326977, 0.3564002514, 0.3613068759, 0.3662526906, 0.3712377846, 0.3762622178, 0.3813261092, 0.3864295185, 0.3915725648, 0.3967553079, 0.4019778669, 0.4072403014, 0.4125427008, 0.4178851545, 0.4232677519, 0.4286905527,
0.4341537058, 0.4396572411, 0.4452012479, 0.4507858455, 0.4564110637, 0.4620770514, 0.4677838385, 0.4735315442, 0.4793202281, 0.4851499796, 0.4910208881, 0.4969330430, 0.5028865933, 0.5088814497, 0.5149177909, 0.5209956765,
0.5271152258, 0.5332764983, 0.5394796133, 0.5457245708, 0.5520114899, 0.5583404899, 0.5647116303, 0.5711249113, 0.5775805116, 0.5840784907, 0.5906189084, 0.5972018838, 0.6038274169, 0.6104956269, 0.6172066331, 0.6239604354,
0.6307572126, 0.6375969648, 0.6444797516, 0.6514056921, 0.6583748460, 0.6653873324, 0.6724432111, 0.6795425415, 0.6866854429, 0.6938719153, 0.7011020184, 0.7083759308, 0.7156936526, 0.7230552435, 0.7304608822, 0.7379105687,
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,
];

82
pkgs/edit/src/path.rs Normal file
View file

@ -0,0 +1,82 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Path related helpers.
use std::ffi::OsStr;
use std::path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf};
/// Normalizes a given path by removing redundant components.
/// The given path must be absolute (e.g. by joining it with the current working directory).
pub fn normalize(path: &Path) -> PathBuf {
debug_assert!(path.is_absolute());
let mut res = PathBuf::with_capacity(path.as_os_str().len());
let mut root_len = 0;
for component in path.components() {
match component {
Component::Prefix(p) => res.push(p.as_os_str()),
Component::RootDir => {
res.push(OsStr::new(MAIN_SEPARATOR_STR));
root_len = res.as_os_str().len();
}
Component::CurDir => {}
Component::ParentDir => {
// Get the length up to the parent directory
if let Some(len) = res
.parent()
.map(|p| p.as_os_str().as_encoded_bytes().len())
// Ensure we don't pop the root directory
&& len >= root_len
{
res.as_mut_os_string().truncate(len);
}
}
Component::Normal(p) => res.push(p),
}
}
res
}
#[cfg(test)]
mod tests {
use std::ffi::OsString;
use std::path::Path;
use super::*;
fn norm(s: &str) -> OsString {
normalize(Path::new(s)).into_os_string()
}
#[cfg(unix)]
#[test]
fn test_unix() {
assert_eq!(norm("/a/b/c"), "/a/b/c");
assert_eq!(norm("/a/b/c/"), "/a/b/c");
assert_eq!(norm("/a/./b"), "/a/b");
assert_eq!(norm("/a/b/../c"), "/a/c");
assert_eq!(norm("/../../a"), "/a");
assert_eq!(norm("/../"), "/");
assert_eq!(norm("/a//b/c"), "/a/b/c");
assert_eq!(norm("/a/b/c/../../../../d"), "/d");
assert_eq!(norm("//"), "/");
}
#[cfg(windows)]
#[test]
fn test_windows() {
assert_eq!(norm(r"C:\a\b\c"), r"C:\a\b\c");
assert_eq!(norm(r"C:\a\b\c\"), r"C:\a\b\c");
assert_eq!(norm(r"C:\a\.\b"), r"C:\a\b");
assert_eq!(norm(r"C:\a\b\..\c"), r"C:\a\c");
assert_eq!(norm(r"C:\..\..\a"), r"C:\a");
assert_eq!(norm(r"C:\..\"), r"C:\");
assert_eq!(norm(r"C:\a\\b\c"), r"C:\a\b\c");
assert_eq!(norm(r"C:/a\b/c"), r"C:\a\b\c");
assert_eq!(norm(r"C:\a\b\c\..\..\..\..\d"), r"C:\d");
assert_eq!(norm(r"\\server\share\path"), r"\\server\share\path");
}
}

View file

@ -0,0 +1,196 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! `memchr`, but with two needles.
use std::ptr;
use super::distance;
/// `memchr`, but with two needles.
///
/// Returns the index of the first occurrence of either needle in the
/// `haystack`. If no needle is found, `haystack.len()` is returned.
/// `offset` specifies the index to start searching from.
pub fn memchr2(needle1: u8, needle2: u8, haystack: &[u8], offset: usize) -> usize {
unsafe {
let beg = haystack.as_ptr();
let end = beg.add(haystack.len());
let it = beg.add(offset.min(haystack.len()));
let it = memchr2_raw(needle1, needle2, it, end);
distance(it, beg)
}
}
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"))]
return unsafe { MEMCHR2_DISPATCH(needle1, needle2, beg, end) };
#[cfg(target_arch = "aarch64")]
return unsafe { memchr2_neon(needle1, needle2, beg, end) };
}
unsafe fn memchr2_fallback(
needle1: u8,
needle2: u8,
mut beg: *const u8,
end: *const u8,
) -> *const u8 {
unsafe {
while !ptr::eq(beg, end) {
let ch = *beg;
if ch == needle1 || ch == needle2 {
break;
}
beg = beg.add(1);
}
beg
}
}
// In order to make `memchr2_raw` slim and fast, we use a function pointer that updates
// 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"))]
static mut MEMCHR2_DISPATCH: unsafe fn(
needle1: u8,
needle2: u8,
beg: *const u8,
end: *const u8,
) -> *const u8 = memchr2_dispatch;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe fn memchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
let func = if is_x86_feature_detected!("avx2") { memchr2_avx2 } else { memchr2_fallback };
unsafe { MEMCHR2_DISPATCH = func };
unsafe { func(needle1, needle2, beg, end) }
}
// FWIW, I found that adding support for AVX512 was not useful at the time,
// as it only marginally improved file load performance by <5%.
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "avx2")]
unsafe fn memchr2_avx2(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
unsafe {
#[cfg(target_arch = "x86")]
use std::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
let n1 = _mm256_set1_epi8(needle1 as i8);
let n2 = _mm256_set1_epi8(needle2 as i8);
let mut remaining = distance(end, beg);
while remaining >= 32 {
let v = _mm256_loadu_si256(beg as *const _);
let a = _mm256_cmpeq_epi8(v, n1);
let b = _mm256_cmpeq_epi8(v, n2);
let c = _mm256_or_si256(a, b);
let m = _mm256_movemask_epi8(c) as u32;
if m != 0 {
return beg.add(m.trailing_zeros() as usize);
}
beg = beg.add(32);
remaining -= 32;
}
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 {
use std::arch::aarch64::*;
if distance(end, beg) >= 16 {
let n1 = vdupq_n_u8(needle1);
let n2 = vdupq_n_u8(needle2);
loop {
let v = vld1q_u8(beg as *const _);
let a = vceqq_u8(v, n1);
let b = vceqq_u8(v, n2);
let c = vorrq_u8(a, b);
// https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon
let m = vreinterpretq_u16_u8(c);
let m = vshrn_n_u16(m, 4);
let m = vreinterpret_u64_u8(m);
let m = vget_lane_u64(m, 0);
if m != 0 {
return beg.add(m.trailing_zeros() as usize >> 2);
}
beg = beg.add(16);
if distance(end, beg) < 16 {
break;
}
}
}
memchr2_fallback(needle1, needle2, beg, end)
}
}
#[cfg(test)]
mod tests {
use std::slice;
use super::*;
use crate::sys;
#[test]
fn test_empty() {
assert_eq!(memchr2(b'a', b'b', b"", 0), 0);
}
#[test]
fn test_basic() {
let haystack = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let haystack = &haystack[..43];
assert_eq!(memchr2(b'a', b'z', haystack, 0), 0);
assert_eq!(memchr2(b'p', b'q', haystack, 0), 15);
assert_eq!(memchr2(b'Q', b'Z', haystack, 0), 42);
assert_eq!(memchr2(b'0', b'9', haystack, 0), haystack.len());
}
// Test that it doesn't match before/after the start offset respectively.
#[test]
fn test_with_offset() {
let haystack = b"abcdefghabcdefghabcdefghabcdefghabcdefgh";
assert_eq!(memchr2(b'a', b'b', haystack, 0), 0);
assert_eq!(memchr2(b'a', b'b', haystack, 1), 1);
assert_eq!(memchr2(b'a', b'b', haystack, 2), 8);
assert_eq!(memchr2(b'a', b'b', haystack, 9), 9);
assert_eq!(memchr2(b'a', b'b', haystack, 16), 16);
assert_eq!(memchr2(b'a', b'b', haystack, 41), 40);
}
// Test memory access safety at page boundaries.
// The test is a success if it doesn't segfault.
#[test]
fn test_page_boundary() {
let page = unsafe {
let page_size = 4096;
// 3 pages: uncommitted, committed, uncommitted
let ptr = sys::virtual_reserve(page_size * 3).unwrap();
sys::virtual_commit(ptr.add(page_size), page_size).unwrap();
slice::from_raw_parts_mut(ptr.add(page_size).as_ptr(), page_size)
};
page.fill(b'a');
// Test if it seeks beyond the page boundary.
assert_eq!(memchr2(b'\0', b'\0', &page[page.len() - 40..], 0), 40);
// Test if it seeks before the page boundary for the masked/partial load.
assert_eq!(memchr2(b'\0', b'\0', &page[..10], 0), 10);
}
}

View file

@ -0,0 +1,196 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! `memchr`, but with two needles.
use std::ptr;
use super::distance;
/// `memchr`, but with two needles.
///
/// If no needle is found, 0 is returned.
/// Unlike `memchr2` (or `memrchr`), an offset PAST the hit is returned.
/// This is because this function is primarily used for
/// `ucd::newlines_backward`, which needs exactly that.
pub fn memrchr2(needle1: u8, needle2: u8, haystack: &[u8], offset: usize) -> Option<usize> {
unsafe {
let beg = haystack.as_ptr();
let it = beg.add(offset.min(haystack.len()));
let it = memrchr2_raw(needle1, needle2, beg, it);
if it.is_null() { None } else { Some(distance(it, beg)) }
}
}
unsafe fn memrchr2_raw(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
return unsafe { MEMRCHR2_DISPATCH(needle1, needle2, beg, end) };
#[cfg(target_arch = "aarch64")]
return unsafe { memrchr2_neon(needle1, needle2, beg, end) };
#[allow(unreachable_code)]
return unsafe { memrchr2_fallback(needle1, needle2, beg, end) };
}
unsafe fn memrchr2_fallback(
needle1: u8,
needle2: u8,
beg: *const u8,
mut end: *const u8,
) -> *const u8 {
unsafe {
while !ptr::eq(end, beg) {
end = end.sub(1);
let ch = *end;
if ch == needle1 || needle2 == ch {
return end;
}
}
ptr::null()
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
static mut MEMRCHR2_DISPATCH: unsafe fn(
needle1: u8,
needle2: u8,
beg: *const u8,
end: *const u8,
) -> *const u8 = memrchr2_dispatch;
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe fn memrchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
let func = if is_x86_feature_detected!("avx2") { memrchr2_avx2 } else { memrchr2_fallback };
unsafe { MEMRCHR2_DISPATCH = func };
unsafe { func(needle1, needle2, beg, end) }
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "avx2")]
unsafe fn memrchr2_avx2(needle1: u8, needle2: u8, beg: *const u8, mut end: *const u8) -> *const u8 {
unsafe {
#[cfg(target_arch = "x86")]
use std::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
if distance(end, beg) >= 32 {
let n1 = _mm256_set1_epi8(needle1 as i8);
let n2 = _mm256_set1_epi8(needle2 as i8);
loop {
end = end.sub(32);
let v = _mm256_loadu_si256(end as *const _);
let a = _mm256_cmpeq_epi8(v, n1);
let b = _mm256_cmpeq_epi8(v, n2);
let c = _mm256_or_si256(a, b);
let m = _mm256_movemask_epi8(c) as u32;
if m != 0 {
return end.add(31 - m.leading_zeros() as usize);
}
if distance(end, beg) < 32 {
break;
}
}
}
memrchr2_fallback(needle1, needle2, beg, end)
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn memrchr2_neon(needle1: u8, needle2: u8, beg: *const u8, mut end: *const u8) -> *const u8 {
unsafe {
use std::arch::aarch64::*;
if distance(end, beg) >= 16 {
let n1 = vdupq_n_u8(needle1);
let n2 = vdupq_n_u8(needle2);
loop {
end = end.sub(16);
let v = vld1q_u8(end as *const _);
let a = vceqq_u8(v, n1);
let b = vceqq_u8(v, n2);
let c = vorrq_u8(a, b);
// https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon
let m = vreinterpretq_u16_u8(c);
let m = vshrn_n_u16(m, 4);
let m = vreinterpret_u64_u8(m);
let m = vget_lane_u64(m, 0);
if m != 0 {
return end.add(15 - (m.leading_zeros() as usize >> 2));
}
if distance(end, beg) < 16 {
break;
}
}
}
memrchr2_fallback(needle1, needle2, beg, end)
}
}
#[cfg(test)]
mod tests {
use std::slice;
use super::*;
use crate::sys;
#[test]
fn test_empty() {
assert_eq!(memrchr2(b'a', b'b', b"", 0), None);
}
#[test]
fn test_basic() {
let haystack = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let haystack = &haystack[..43];
assert_eq!(memrchr2(b'Q', b'P', haystack, 43), Some(42));
assert_eq!(memrchr2(b'p', b'o', haystack, 43), Some(15));
assert_eq!(memrchr2(b'a', b'b', haystack, 43), Some(1));
assert_eq!(memrchr2(b'0', b'9', haystack, 43), None);
}
// Test that it doesn't match before/after the start offset respectively.
#[test]
fn test_with_offset() {
let haystack = b"abcdefghabcdefghabcdefghabcdefghabcdefgh";
assert_eq!(memrchr2(b'h', b'g', haystack, 40), Some(39));
assert_eq!(memrchr2(b'h', b'g', haystack, 39), Some(38));
assert_eq!(memrchr2(b'a', b'b', haystack, 9), Some(8));
assert_eq!(memrchr2(b'a', b'b', haystack, 1), Some(0));
assert_eq!(memrchr2(b'a', b'b', haystack, 0), None);
}
// Test memory access safety at page boundaries.
// The test is a success if it doesn't segfault.
#[test]
fn test_page_boundary() {
let page = unsafe {
let page_size = 4096;
// 3 pages: uncommitted, committed, uncommitted
let ptr = sys::virtual_reserve(page_size * 3).unwrap();
sys::virtual_commit(ptr.add(page_size), page_size).unwrap();
slice::from_raw_parts_mut(ptr.add(page_size).as_ptr(), page_size)
};
page.fill(b'a');
// Same as above, but for memrchr2 (hence reversed).
assert_eq!(memrchr2(b'\0', b'\0', &page[page.len() - 10..], 10), None);
assert_eq!(memrchr2(b'\0', b'\0', &page[..40], 40), None);
}
}

View file

@ -0,0 +1,345 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! `memchr` for arbitrary sizes (1/2/4/8 bytes).
//!
//! Clang calls the C `memset` function only for byte-sized types (or 0 fills).
//! We however need to fill other types as well. For that, clang generates
//! SIMD loops under higher optimization levels. With `-Os` however, it only
//! generates a trivial loop which is too slow for our needs.
//!
//! This implementation uses SWAR to only have a single implementation for all
//! 4 sizes: By duplicating smaller types into a larger `u64` register we can
//! treat all sizes as if they were `u64`. The only thing we need to take care
//! of is the tail end of the array, which needs to write 0-7 additional bytes.
use std::mem;
use super::distance;
/// A marker trait for types that are safe to `memset`.
///
/// # Safety
///
/// Just like with C's `memset`, bad things happen
/// if you use this with non-trivial types.
pub unsafe trait MemsetSafe: Copy {}
unsafe impl MemsetSafe for u8 {}
unsafe impl MemsetSafe for u16 {}
unsafe impl MemsetSafe for u32 {}
unsafe impl MemsetSafe for u64 {}
unsafe impl MemsetSafe for usize {}
unsafe impl MemsetSafe for i8 {}
unsafe impl MemsetSafe for i16 {}
unsafe impl MemsetSafe for i32 {}
unsafe impl MemsetSafe for i64 {}
unsafe impl MemsetSafe for isize {}
/// Fills a slice with the given value.
#[inline]
pub fn memset<T: MemsetSafe>(dst: &mut [T], val: T) {
unsafe {
match mem::size_of::<T>() {
1 => {
// LLVM will compile this to a call to `memset`,
// which hopefully should be better optimized than my code.
let beg = dst.as_mut_ptr();
let val = mem::transmute_copy::<_, u8>(&val);
beg.write_bytes(val, dst.len());
}
2 => {
let beg = dst.as_mut_ptr();
let end = beg.add(dst.len());
let val = mem::transmute_copy::<_, u16>(&val);
memset_raw(beg as *mut u8, end as *mut u8, val as u64 * 0x0001000100010001);
}
4 => {
let beg = dst.as_mut_ptr();
let end = beg.add(dst.len());
let val = mem::transmute_copy::<_, u32>(&val);
memset_raw(beg as *mut u8, end as *mut u8, val as u64 * 0x0000000100000001);
}
8 => {
let beg = dst.as_mut_ptr();
let end = beg.add(dst.len());
let val = mem::transmute_copy::<_, u64>(&val);
memset_raw(beg as *mut u8, end as *mut u8, val);
}
_ => unreachable!(),
}
}
}
#[inline]
fn memset_raw(beg: *mut u8, end: *mut u8, val: u64) {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
return unsafe { MEMSET_DISPATCH(beg, end, val) };
#[cfg(target_arch = "aarch64")]
return unsafe { memset_neon(beg, end, val) };
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
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"))]
fn memset_dispatch(beg: *mut u8, end: *mut u8, val: u64) {
let func = if is_x86_feature_detected!("avx2") { memset_avx2 } else { memset_sse2 };
unsafe { MEMSET_DISPATCH = func };
unsafe { func(beg, end, val) }
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "sse2")]
unsafe fn memset_sse2(mut beg: *mut u8, end: *mut u8, val: u64) {
unsafe {
#[cfg(target_arch = "x86")]
use std::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
let mut remaining = distance(end, beg);
if remaining >= 16 {
let fill = _mm_set1_epi64x(val as i64);
while remaining >= 32 {
_mm_storeu_si128(beg as *mut _, fill);
_mm_storeu_si128(beg.add(16) as *mut _, fill);
beg = beg.add(32);
remaining -= 32;
}
if remaining >= 16 {
// 16-31 bytes remaining
_mm_storeu_si128(beg as *mut _, fill);
_mm_storeu_si128(end.sub(16) as *mut _, fill);
return;
}
}
if remaining >= 8 {
// 8-15 bytes remaining
(beg as *mut u64).write_unaligned(val);
(end.sub(8) as *mut u64).write_unaligned(val);
} else if remaining >= 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 remaining >= 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 remaining >= 1 {
// 1 byte remaining
beg.write(val as u8);
}
}
}
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "avx2")]
fn memset_avx2(mut beg: *mut u8, end: *mut u8, val: u64) {
unsafe {
#[cfg(target_arch = "x86")]
use std::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;
use std::hint::black_box;
let mut remaining = distance(end, beg);
if remaining >= 128 {
let fill = _mm256_set1_epi64x(val as i64);
loop {
_mm256_storeu_si256(beg as *mut _, fill);
_mm256_storeu_si256(beg.add(32) as *mut _, fill);
_mm256_storeu_si256(beg.add(64) as *mut _, fill);
_mm256_storeu_si256(beg.add(96) as *mut _, fill);
beg = beg.add(128);
remaining -= 128;
if remaining < 128 {
break;
}
}
}
if remaining >= 16 {
let fill = _mm_set1_epi64x(val as i64);
loop {
// LLVM is _very_ eager to unroll loops. In the absence of an unroll attribute, black_box does the job.
// Note that this must not be applied to the intrinsic parameters, as they're otherwise misoptimized.
#[allow(clippy::unit_arg)]
black_box(_mm_storeu_si128(beg as *mut _, fill));
beg = beg.add(16);
remaining -= 16;
if remaining < 16 {
break;
}
}
}
// `remaining` is between 0 and 15 at this point.
// By overlapping the stores we can write all of them in at most 2 stores. This approach
// can be seen in various libraries, such as wyhash which uses it for loading data in `wyr3`.
if remaining >= 8 {
// 8-15 bytes
(beg as *mut u64).write_unaligned(val);
(end.sub(8) as *mut u64).write_unaligned(val);
} else if remaining >= 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 remaining >= 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 remaining >= 1 {
// 1 byte
beg.write(val as u8);
}
}
}
#[cfg(target_arch = "aarch64")]
unsafe fn memset_neon(mut beg: *mut u8, end: *mut u8, val: u64) {
unsafe {
use std::arch::aarch64::*;
let mut remaining = distance(end, beg);
if remaining >= 32 {
let fill = vdupq_n_u64(val);
loop {
// Compiles to a single `stp` instruction.
vst1q_u64(beg as *mut _, fill);
vst1q_u64(beg.add(16) as *mut _, fill);
beg = beg.add(32);
remaining -= 32;
if remaining < 32 {
break;
}
}
}
if remaining >= 16 {
// 16-31 bytes remaining
let fill = vdupq_n_u64(val);
vst1q_u64(beg as *mut _, fill);
vst1q_u64(end.sub(16) as *mut _, fill);
} else if remaining >= 8 {
// 8-15 bytes remaining
(beg as *mut u64).write_unaligned(val);
(end.sub(8) as *mut u64).write_unaligned(val);
} else if remaining >= 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 remaining >= 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 remaining >= 1 {
// 1 byte remaining
beg.write(val as u8);
}
}
}
#[cfg(test)]
mod tests {
use std::fmt;
use std::ops::Not;
use super::*;
fn check_memset<T>(val: T, len: usize)
where
T: MemsetSafe + Not<Output = T> + PartialEq + fmt::Debug,
{
let mut buf = vec![!val; len];
memset(&mut buf, val);
assert!(buf.iter().all(|&x| x == val));
}
#[test]
fn test_memset_empty() {
check_memset(0u8, 0);
check_memset(0u16, 0);
check_memset(0u32, 0);
check_memset(0u64, 0);
}
#[test]
fn test_memset_single() {
check_memset(0u8, 1);
check_memset(0xFFu8, 1);
check_memset(0xABu16, 1);
check_memset(0x12345678u32, 1);
check_memset(0xDEADBEEFu64, 1);
}
#[test]
fn test_memset_small() {
for &len in &[2, 3, 4, 5, 7, 8, 9] {
check_memset(0xAAu8, len);
check_memset(0xBEEFu16, len);
check_memset(0xCAFEBABEu32, len);
check_memset(0x1234567890ABCDEFu64, len);
}
}
#[test]
fn test_memset_large() {
check_memset(0u8, 1000);
check_memset(0xFFu8, 1024);
check_memset(0xBEEFu16, 512);
check_memset(0xCAFEBABEu32, 256);
check_memset(0x1234567890ABCDEFu64, 128);
}
#[test]
fn test_memset_various_values() {
check_memset(0u8, 17);
check_memset(0x7Fu8, 17);
check_memset(0x8001u16, 17);
check_memset(0xFFFFFFFFu32, 17);
check_memset(0x8000000000000001u64, 17);
}
#[test]
fn test_memset_signed_types() {
check_memset(-1i8, 8);
check_memset(-2i16, 8);
check_memset(-3i32, 8);
check_memset(-4i64, 8);
check_memset(-5isize, 8);
}
#[test]
fn test_memset_usize_isize() {
check_memset(0usize, 4);
check_memset(usize::MAX, 4);
check_memset(0isize, 4);
check_memset(isize::MIN, 4);
}
#[test]
fn test_memset_alignment() {
// Check that memset works for slices not aligned to 8 bytes
let mut buf = [0u8; 15];
for offset in 0..8 {
let slice = &mut buf[offset..(offset + 7)];
memset(slice, 0x5A);
assert!(slice.iter().all(|&x| x == 0x5A));
}
}
}

18
pkgs/edit/src/simd/mod.rs Normal file
View file

@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Provides various high-throughput utilities.
mod memchr2;
mod memrchr2;
mod memset;
pub use memchr2::*;
pub use memrchr2::*;
pub use memset::*;
// Can be replaced with `sub_ptr` once it's stabilized.
#[inline(always)]
unsafe fn distance<T>(hi: *const T, lo: *const T) -> usize {
unsafe { usize::try_from(hi.offset_from(lo)).unwrap_unchecked() }
}

28
pkgs/edit/src/sys/mod.rs Normal file
View file

@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Platform abstractions.
use std::fs::File;
use std::path::Path;
use crate::apperr;
#[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::*;
pub fn file_id_at(path: &Path) -> apperr::Result<FileId> {
let file = File::open(path)?;
let file_id = file_id(&file)?;
Ok(file_id)
}

569
pkgs/edit/src/sys/unix.rs Normal file
View file

@ -0,0 +1,569 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Unix-specific platform code.
//!
//! 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::mem::{self, MaybeUninit};
use std::os::fd::{AsRawFd as _, FromRawFd as _};
use std::ptr::{self, NonNull, null, null_mut};
use std::{thread, time};
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;
use crate::{apperr, arena_format};
struct State {
stdin: libc::c_int,
stdin_flags: libc::c_int,
stdout: libc::c_int,
stdout_initial_termios: Option<libc::termios>,
inject_resize: bool,
// Buffer for incomplete UTF-8 sequences (max 4 bytes needed)
utf8_buf: [u8; 4],
utf8_len: usize,
}
static mut STATE: State = State {
stdin: libc::STDIN_FILENO,
stdin_flags: 0,
stdout: libc::STDOUT_FILENO,
stdout_initial_termios: None,
inject_resize: false,
utf8_buf: [0; 4],
utf8_len: 0,
};
extern "C" fn sigwinch_handler(_: libc::c_int) {
unsafe {
STATE.inject_resize = true;
}
}
pub fn init() -> apperr::Result<Deinit> {
unsafe {
// Reopen stdin if it's redirected (= piped input).
if libc::isatty(STATE.stdin) == 0 {
STATE.stdin = check_int_return(libc::open(c"/dev/tty".as_ptr(), libc::O_RDONLY))?;
}
// 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;
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()))?;
let mut termios = termios.assume_init();
STATE.stdout_initial_termios = Some(termios);
termios.c_iflag &= !(
// When neither IGNBRK...
libc::IGNBRK
// ...nor BRKINT are set, a BREAK reads as a null byte ('\0'), ...
| libc::BRKINT
// ...except when PARMRK is set, in which case it reads as the sequence \377 \0 \0.
| libc::PARMRK
// Disable input parity checking.
| libc::INPCK
// Disable stripping of eighth bit.
| libc::ISTRIP
// Disable mapping of NL to CR on input.
| libc::INLCR
// Disable ignoring CR on input.
| libc::IGNCR
// Disable mapping of CR to NL on input.
| libc::ICRNL
// Disable software flow control.
| libc::IXON
);
// Disable output processing.
termios.c_oflag &= !libc::OPOST;
termios.c_cflag &= !(
// Reset character size mask.
libc::CSIZE
// Disable parity generation.
| libc::PARENB
);
// Set character size back to 8 bits.
termios.c_cflag |= libc::CS8;
termios.c_lflag &= !(
// Disable signal generation (SIGINT, SIGTSTP, SIGQUIT).
libc::ISIG
// Disable canonical mode (line buffering).
| libc::ICANON
// Disable echoing of input characters.
| libc::ECHO
// Disable echoing of NL.
| libc::ECHONL
// Disable extended input processing (e.g. Ctrl-V).
| libc::IEXTEN
);
// Set the terminal to raw mode.
termios.c_lflag &= !(libc::ICANON | libc::ECHO);
check_int_return(libc::tcsetattr(STATE.stdin, libc::TCSANOW, &termios))?;
Ok(())
}
}
pub fn inject_window_size_into_stdin() {
unsafe {
STATE.inject_resize = true;
}
}
fn get_window_size() -> (u16, u16) {
let mut winsz: libc::winsize = unsafe { mem::zeroed() };
for attempt in 1.. {
let ret = unsafe { libc::ioctl(STATE.stdout, libc::TIOCGWINSZ, &raw mut winsz) };
if ret == -1 || (winsz.ws_col != 0 && winsz.ws_row != 0) {
break;
}
if attempt == 10 {
winsz.ws_col = 80;
winsz.ws_row = 24;
break;
}
// Some terminals are bad emulators and don't report TIOCGWINSZ immediately.
thread::sleep(time::Duration::from_millis(10 * attempt));
}
(winsz.ws_col, winsz.ws_row)
}
/// Reads from stdin.
///
/// Returns `None` if there was an error reading from stdin.
/// Returns `Some("")` if the given timeout was reached.
/// Otherwise, it returns the read, non-empty string.
pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaString<'_>> {
unsafe {
if STATE.inject_resize {
timeout = time::Duration::ZERO;
}
let read_poll = timeout != time::Duration::MAX;
let mut buf = Vec::new_in(arena);
// We don't know if the input is valid UTF8, so we first use a Vec and then
// later turn it into UTF8 using `from_utf8_lossy_owned`.
// It is important that we allocate the buffer with an explicit capacity,
// because we later use `spare_capacity_mut` to access it.
buf.reserve(4 * KIBI);
// 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]);
}
loop {
if timeout != time::Duration::MAX {
let beg = time::Instant::now();
let mut pollfd = libc::pollfd { fd: STATE.stdin, events: libc::POLLIN, revents: 0 };
let ts = libc::timespec {
tv_sec: timeout.as_secs() as libc::time_t,
tv_nsec: timeout.subsec_nanos() as libc::c_long,
};
let ret = libc::ppoll(&mut pollfd, 1, &ts, null());
if ret < 0 {
return None; // Error? Let's assume it's an EOF.
}
if ret == 0 {
break; // Timeout? We can stop reading.
}
timeout = timeout.saturating_sub(beg.elapsed());
};
// If we're asked for a non-blocking read we need
// to manipulate `O_NONBLOCK` and vice versa.
set_tty_nonblocking(read_poll);
// Read from stdin.
let spare = buf.spare_capacity_mut();
let ret = libc::read(STATE.stdin, spare.as_mut_ptr() as *mut _, spare.len());
if ret > 0 {
buf.set_len(buf.len() + ret as usize);
break;
}
if ret == 0 {
return None; // EOF
}
if ret < 0 {
match *libc::__errno_location() {
libc::EINTR if STATE.inject_resize => break,
libc::EAGAIN if timeout == time::Duration::ZERO => break,
libc::EINTR | libc::EAGAIN => {}
_ => return None,
}
}
}
if !buf.is_empty() {
// We only need to check the last 3 bytes for UTF-8 continuation bytes,
// because we should be able to assume that any 4 byte sequence is complete.
let lim = buf.len().saturating_sub(3);
let mut off = buf.len() - 1;
// Find the start of the last potentially incomplete UTF-8 sequence.
while off > lim && buf[off] & 0b1100_0000 == 0b1000_0000 {
off -= 1;
}
let seq_len = match buf[off] {
b if b & 0b1000_0000 == 0 => 1,
b if b & 0b1110_0000 == 0b1100_0000 => 2,
b if b & 0b1111_0000 == 0b1110_0000 => 3,
b if b & 0b1111_1000 == 0b1111_0000 => 4,
// If the lead byte we found isn't actually one, we don't cache it.
// `from_utf8_lossy_owned` will replace it with U+FFFD.
_ => 0,
};
// Cache incomplete sequence if any.
if off + seq_len > buf.len() {
STATE.utf8_len = buf.len() - off;
STATE.utf8_buf[..STATE.utf8_len].copy_from_slice(&buf[off..]);
buf.truncate(off);
}
}
let mut result = ArenaString::from_utf8_lossy_owned(buf);
// We received a SIGWINCH? Add a fake window size sequence for our input parser.
// I prepend it so that on startup, the TUI system gets first initialized with a size.
if STATE.inject_resize {
STATE.inject_resize = false;
let (w, h) = get_window_size();
if w > 0 && h > 0 {
let scratch = scratch_arena(Some(arena));
let seq = arena_format!(&scratch, "\x1b[8;{h};{w}t");
result.replace_range(0..0, &seq);
}
}
result.shrink_to_fit();
Some(result)
}
}
pub fn write_stdout(text: &str) {
if text.is_empty() {
return;
}
// If we don't set the TTY to blocking mode,
// the write will potentially fail with EAGAIN.
set_tty_nonblocking(false);
let buf = text.as_bytes();
let mut written = 0;
while written < buf.len() {
let w = &buf[written..];
let w = &buf[..w.len().min(GIBI)];
let n = unsafe { libc::write(STATE.stdout, w.as_ptr() as *const _, w.len()) };
if n >= 0 {
written += n as usize;
continue;
}
let err = unsafe { *libc::__errno_location() };
if err != libc::EINTR {
return;
}
}
}
/// Sets/Resets `O_NONBLOCK` on the TTY handle.
///
/// Note that setting this flag applies to both stdin and stdout, because the
/// TTY is a bidirectional device and both handles refer to the same thing.
fn set_tty_nonblocking(nonblock: bool) {
unsafe {
let is_nonblock = (STATE.stdin_flags & libc::O_NONBLOCK) != 0;
if is_nonblock != nonblock {
STATE.stdin_flags ^= libc::O_NONBLOCK;
let _ = libc::fcntl(STATE.stdin, libc::F_SETFL, STATE.stdin_flags);
}
}
}
pub fn open_stdin_if_redirected() -> Option<File> {
unsafe {
// Did we reopen stdin during `init()`?
if STATE.stdin != libc::STDIN_FILENO {
Some(File::from_raw_fd(libc::STDIN_FILENO))
} else {
None
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct FileId {
st_dev: libc::dev_t,
st_ino: libc::ino_t,
}
/// Returns a unique identifier for the given file.
pub fn file_id(file: &File) -> apperr::Result<FileId> {
unsafe {
let mut stat = MaybeUninit::<libc::stat>::uninit();
check_int_return(libc::fstat(file.as_raw_fd(), stat.as_mut_ptr()))?;
let stat = stat.assume_init();
Ok(FileId { st_dev: stat.st_dev, st_ino: stat.st_ino })
}
}
/// 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 {
let ptr = libc::mmap(
null_mut(),
size,
libc::PROT_NONE,
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::ELIBACC))
}
}
/// Loads a function from a dynamic library.
///
/// # Safety
///
/// This function is highly unsafe as it requires you to know the exact type
/// 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> {
unsafe {
let sym = libc::dlsym(handle.as_ptr(), name.as_ptr());
if sym.is_null() {
Err(errno_to_apperr(libc::ELIBACC))
} else {
Ok(mem::transmute_copy(&sym))
}
}
}
pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(c"libicuuc.so") }
}
pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(c"libicui18n.so") }
}
/// 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<'_> {
unsafe {
type T = *const c_void;
let mut res = ArenaString::new_in(arena);
// 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() {
return res;
}
// In the versions (63-76) and distributions (Arch/Debian) I tested,
// 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") {
Ok(proc) => proc,
Err(_) => return res,
};
// `dladdr` is specific to GNU's libc unfortunately.
let mut info: libc::Dl_info = mem::zeroed();
let ret = libc::dladdr(proc, &mut info);
if ret == 0 {
return res;
}
// The library path is in `info.dli_fname`.
let path = match CStr::from_ptr(info.dli_fname).to_str() {
Ok(name) => name,
Err(_) => return res,
};
let path = match fs::read_link(path) {
Ok(path) => path,
Err(_) => path.into(),
};
// I'm going to assume it's something like "libicuuc.so.76.1".
let path = path.into_os_string();
let path = path.to_string_lossy();
let suffix_start = match path.rfind(".so.") {
Some(pos) => pos + 4,
None => return res,
};
let version = &path[suffix_start..];
let version_end = version.find('.').unwrap_or(version.len());
let version = &version[..version_end];
res.push('_');
res.push_str(version);
res
}
}
pub fn add_icu_proc_suffix<'a, 'b, 'r>(arena: &'a Arena, name: &'b CStr, suffix: &str) -> &'r CStr
where
'a: 'r,
'b: 'r,
{
if suffix.is_empty() {
name
} else {
// SAFETY: In this particualar case we know that the string
// is valid UTF-8, because it comes from icu.rs.
let name = unsafe { name.to_str().unwrap_unchecked() };
let mut res = 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) }
}
}
pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString<'_>, &Arena> {
let mut locales = Vec::new_in(arena);
for key in ["LANGUAGE", "LC_ALL", "LANG"] {
if let Ok(val) = std::env::var(key) {
locales.extend(
val.split(':').filter(|s| !s.is_empty()).map(|s| ArenaString::from_str(arena, s)),
);
break;
}
}
locales
}
#[inline]
pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
errno_to_apperr(err.raw_os_error().unwrap_or(0))
}
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(())
}
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(unsafe { *libc::__errno_location() })) } else { Ok(ret) }
}

View file

@ -0,0 +1,680 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::{CStr, OsString, 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 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::{Foundation, Globalization};
use windows_sys::w;
use crate::apperr;
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;
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;
unsafe extern "system" fn read_console_input_ex_placeholder(
_: Foundation::HANDLE,
_: *mut Console::INPUT_RECORD,
_: u32,
_: *mut u32,
_: u16,
) -> Foundation::BOOL {
panic!();
}
const CONSOLE_READ_NOWAIT: u16 = 0x0002;
const INVALID_CONSOLE_MODE: u32 = u32::MAX;
struct State {
read_console_input_ex: ReadConsoleInputExW,
stdin: Foundation::HANDLE,
stdout: Foundation::HANDLE,
stdin_cp_old: u32,
stdout_cp_old: u32,
stdin_mode_old: u32,
stdout_mode_old: u32,
leading_surrogate: u16,
inject_resize: bool,
wants_exit: bool,
}
static mut STATE: State = State {
read_console_input_ex: read_console_input_ex_placeholder,
stdin: null_mut(),
stdout: null_mut(),
stdin_cp_old: 0,
stdout_cp_old: 0,
stdin_mode_old: INVALID_CONSOLE_MODE,
stdout_mode_old: INVALID_CONSOLE_MODE,
leading_surrogate: 0,
inject_resize: false,
wants_exit: false,
};
extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL {
unsafe {
STATE.wants_exit = true;
IO::CancelIoEx(STATE.stdin, null());
}
1
}
/// Initializes the platform-specific state.
pub fn init() -> apperr::Result<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);
// 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
)
{
STATE.stdin = FileSystem::CreateFileW(
w!("CONIN$"),
Foundation::GENERIC_READ | Foundation::GENERIC_WRITE,
FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE,
null_mut(),
FileSystem::OPEN_EXISTING,
0,
null_mut(),
);
}
if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
|| ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE)
{
return Err(get_last_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")) }
}
// `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),
},
};
Ok(Deinit)
}
}
pub struct Deinit;
impl Drop for Deinit {
fn drop(&mut self) {
unsafe {
if STATE.stdin_cp_old != 0 {
Console::SetConsoleCP(STATE.stdin_cp_old);
STATE.stdin_cp_old = 0;
}
if STATE.stdout_cp_old != 0 {
Console::SetConsoleOutputCP(STATE.stdout_cp_old);
STATE.stdout_cp_old = 0;
}
if STATE.stdin_mode_old != INVALID_CONSOLE_MODE {
Console::SetConsoleMode(STATE.stdin, STATE.stdin_mode_old);
STATE.stdin_mode_old = INVALID_CONSOLE_MODE;
}
if STATE.stdout_mode_old != INVALID_CONSOLE_MODE {
Console::SetConsoleMode(STATE.stdout, STATE.stdout_mode_old);
STATE.stdout_mode_old = INVALID_CONSOLE_MODE;
}
}
}
}
/// 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
/// the input parser and provided to the TUI code.
pub fn inject_window_size_into_stdin() {
unsafe {
STATE.inject_resize = true;
}
}
fn get_console_size() -> Option<Size> {
unsafe {
let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed();
info.cbSize = mem::size_of::<Console::CONSOLE_SCREEN_BUFFER_INFOEX>() as u32;
if Console::GetConsoleScreenBufferInfoEx(STATE.stdout, &mut info) == 0 {
return None;
}
let w = (info.srWindow.Right - info.srWindow.Left + 1).max(1) as CoordType;
let h = (info.srWindow.Bottom - info.srWindow.Top + 1).max(1) as CoordType;
Some(Size { width: w, height: h })
}
}
/// Reads from stdin.
///
/// # Returns
///
/// * `None` if there was an error reading from stdin.
/// * `Some("")` if the given timeout was reached.
/// * Otherwise, it returns the read, non-empty string.
pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaString<'_>> {
let scratch = scratch_arena(Some(arena));
// On startup we're asked to inject a window size so that the UI system can layout the elements.
// --> Inject a fake sequence for our input parser.
let mut resize_event = None;
if unsafe { STATE.inject_resize } {
unsafe { STATE.inject_resize = false };
timeout = time::Duration::ZERO;
resize_event = get_console_size();
}
let read_poll = timeout != time::Duration::MAX; // there is a timeout -> don't block in read()
let input_buf = scratch.alloc_uninit_slice(4 * KIBI);
let mut input_buf_cap = input_buf.len();
let utf16_buf = scratch.alloc_uninit_slice(4 * KIBI);
let mut utf16_buf_len = 0;
// If there was a leftover leading surrogate from the last read, we prepend it to the buffer.
if unsafe { STATE.leading_surrogate } != 0 {
utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate });
utf16_buf_len = 1;
input_buf_cap -= 1;
unsafe { STATE.leading_surrogate = 0 };
}
// Read until there's either a timeout or we have something to process.
loop {
if timeout != time::Duration::MAX {
let beg = time::Instant::now();
match unsafe { Threading::WaitForSingleObject(STATE.stdin, timeout.as_millis() as u32) }
{
// Ready to read? Continue with reading below.
Foundation::WAIT_OBJECT_0 => {}
// Timeout? Skip reading entirely.
Foundation::WAIT_TIMEOUT => break,
// Error? Tell the caller stdin is broken.
_ => return None,
}
timeout = timeout.saturating_sub(beg.elapsed());
}
// Read from stdin.
let input = unsafe {
// If we had a `inject_resize`, we don't want to block indefinitely for other pending input on startup,
// but are still interested in any other pending input that may be waiting for us.
let flags = if read_poll { CONSOLE_READ_NOWAIT } else { 0 };
let mut read = 0;
let ok = (STATE.read_console_input_ex)(
STATE.stdin,
input_buf[0].as_mut_ptr(),
input_buf_cap as u32,
&mut read,
flags,
);
if ok == 0 || STATE.wants_exit {
return None;
}
input_buf[..read as usize].assume_init_ref()
};
// Convert Win32 input records into UTF16.
for inp in input {
match inp.EventType as u32 {
Console::KEY_EVENT => {
let event = unsafe { &inp.Event.KeyEvent };
let ch = unsafe { event.uChar.UnicodeChar };
if event.bKeyDown != 0 && ch != 0 {
utf16_buf[utf16_buf_len] = MaybeUninit::new(ch);
utf16_buf_len += 1;
}
}
Console::WINDOW_BUFFER_SIZE_EVENT => {
let event = unsafe { &inp.Event.WindowBufferSizeEvent };
let w = event.dwSize.X as CoordType;
let h = event.dwSize.Y as CoordType;
// Windows is prone to sending broken/useless `WINDOW_BUFFER_SIZE_EVENT`s.
// E.g. starting conhost will emit 3 in a row. Skip rendering in that case.
if w > 0 && h > 0 {
resize_event = Some(Size { width: w, height: h });
}
}
_ => {}
}
}
if resize_event.is_some() || utf16_buf_len != 0 {
break;
}
}
const RESIZE_EVENT_FMT_MAX_LEN: usize = 16; // "\x1b[8;65535;65535t"
let resize_event_len = if resize_event.is_some() { RESIZE_EVENT_FMT_MAX_LEN } else { 0 };
// +1 to account for a potential `STATE.leading_surrogate`.
let utf8_max_len = (utf16_buf_len + 1) * 3;
let mut text = ArenaString::new_in(arena);
text.reserve(utf8_max_len + resize_event_len);
// Now prepend our previously extracted resize event.
if let Some(resize_event) = resize_event {
// If I read xterm's documentation correctly, CSI 18 t reports the window size in characters.
// CSI 8 ; height ; width t is the response. Of course, we didn't send the request,
// but we can use this fake response to trigger the editor to resize itself.
_ = write!(text, "\x1b[8;{};{}t", resize_event.height, resize_event.width);
}
// If the input ends with a lone lead surrogate, we need to remember it for the next read.
if utf16_buf_len > 0 {
unsafe {
let last_char = utf16_buf[utf16_buf_len - 1].assume_init();
if (0xD800..0xDC00).contains(&last_char) {
STATE.leading_surrogate = last_char;
utf16_buf_len -= 1;
}
}
}
// Convert the remaining input to UTF8, the sane encoding.
if utf16_buf_len > 0 {
unsafe {
let vec = text.as_mut_vec();
let spare = vec.spare_capacity_mut();
let len = Globalization::WideCharToMultiByte(
Globalization::CP_UTF8,
0,
utf16_buf[0].as_ptr(),
utf16_buf_len as i32,
spare.as_mut_ptr() as *mut _,
spare.len() as i32,
null(),
null_mut(),
);
if len > 0 {
vec.set_len(vec.len() + len as usize);
}
}
}
text.shrink_to_fit();
Some(text)
}
/// Writes a string to stdout.
///
/// Use this instead of `print!` or `println!` to avoid
/// the overhead of Rust's stdio handling. Don't need that.
pub fn write_stdout(text: &str) {
unsafe {
let mut offset = 0;
while offset < text.len() {
let ptr = text.as_ptr().add(offset);
let write = (text.len() - offset).min(GIBI) as u32;
let mut written = 0;
let ok = FileSystem::WriteFile(STATE.stdout, ptr, write, &mut written, null_mut());
offset += written as usize;
if ok == 0 || written == 0 {
break;
}
}
}
}
/// Check if the stdin handle is redirected to a file, etc.
///
/// # Returns
///
/// * `Some(file)` if stdin is redirected.
/// * Otherwise, `None`.
pub fn open_stdin_if_redirected() -> Option<File> {
unsafe {
let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
// Did we reopen stdin during `init()`?
if !std::ptr::eq(STATE.stdin, handle) { Some(File::from_raw_handle(handle)) } else { None }
}
}
/// A unique identifier for a file.
#[derive(Clone)]
#[repr(transparent)]
pub struct FileId(FileSystem::FILE_ID_INFO);
impl PartialEq for FileId {
fn eq(&self, other: &Self) -> bool {
// Lowers to an efficient word-wise comparison.
const SIZE: usize = std::mem::size_of::<FileSystem::FILE_ID_INFO>();
let a: &[u8; SIZE] = unsafe { mem::transmute(&self.0) };
let b: &[u8; SIZE] = unsafe { mem::transmute(&other.0) };
a == b
}
}
impl Eq for FileId {}
/// Returns a unique identifier for the given file.
pub fn file_id(file: &File) -> apperr::Result<FileId> {
unsafe {
let mut info = MaybeUninit::<FileSystem::FILE_ID_INFO>::uninit();
check_bool_return(FileSystem::GetFileInformationByHandleEx(
file.as_raw_handle(),
FileSystem::FileIdInfo,
info.as_mut_ptr() as *mut _,
mem::size_of::<FileSystem::FILE_ID_INFO>() as u32,
))?;
Ok(FileId(info.assume_init()))
}
}
/// Canonicalizes the given path.
///
/// This differs from [`fs::canonicalize`] in that it strips the `\\?\` UNC
/// prefix on Windows. This is because it's confusing/ugly when displaying it.
pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
let mut path = fs::canonicalize(path)?;
let path = path.as_mut_os_string();
let mut path = mem::take(path).into_encoded_bytes();
if path.len() > 6 && &path[0..4] == br"\\?\" && path[4].is_ascii_uppercase() && path[5] == b':'
{
path.drain(0..4);
}
let path = unsafe { OsString::from_encoded_bytes_unchecked(path) };
let path = PathBuf::from(path);
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 {
Memory::VirtualFree(base.as_ptr() as *mut _, size, 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 { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) }
}
unsafe fn load_library(name: *const u16) -> apperr::Result<NonNull<c_void>> {
unsafe {
check_ptr_return(LibraryLoader::LoadLibraryExW(
name,
null_mut(),
LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32,
))
}
}
/// Loads a function from a dynamic library.
///
/// # Safety
///
/// This function is highly unsafe as it requires you to know the exact type
/// 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> {
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()) }
}
}
/// Loads the "common" portion of ICU4C.
pub fn load_libicuuc() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(w!("icuuc.dll")) }
}
/// Loads the internationalization portion of ICU4C.
pub fn load_libicui18n() -> apperr::Result<NonNull<c_void>> {
unsafe { load_library(w!("icuin.dll")) }
}
/// Returns a list of preferred languages for the current user.
pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString, &Arena> {
// If the GetUserPreferredUILanguages() don't fit into 512 characters,
// honestly, just give up. How many languages do you realistically need?
const LEN: usize = 512;
let scratch = scratch_arena(Some(arena));
let mut res = Vec::new_in(arena);
// Get the list of preferred languages via `GetUserPreferredUILanguages`.
let langs = unsafe {
let buf = scratch.alloc_uninit_slice(LEN);
let mut len = buf.len() as u32;
let mut num = 0;
let ok = Globalization::GetUserPreferredUILanguages(
Globalization::MUI_LANGUAGE_NAME,
&mut num,
buf[0].as_mut_ptr(),
&mut len,
);
if ok == 0 || num == 0 {
len = 0;
}
// Drop the terminating double-null character.
len = len.saturating_sub(1);
buf[..len as usize].assume_init_ref()
};
// Convert UTF16 to UTF8.
let mut langs = wide_to_utf8(&scratch, langs);
// Turn "de-DE" into "de-de" for easier comparisons.
langs.make_ascii_lowercase();
// Split the null-delimited string into individual chunks
// and copy them into the given arena.
res.extend(
langs
.split_terminator('\0')
.filter(|s| !s.is_empty())
.map(|s| ArenaString::from_str(arena, s)),
);
res
}
fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> {
let mut res = ArenaString::new_in(arena);
res.reserve(wide.len() * 3);
let len = unsafe {
Globalization::WideCharToMultiByte(
Globalization::CP_UTF8,
0,
wide.as_ptr(),
wide.len() as i32,
res.as_mut_ptr() as *mut _,
res.capacity() as i32,
null(),
null_mut(),
)
};
if len > 0 {
unsafe { res.as_mut_vec().set_len(len as usize) };
}
res.shrink_to_fit();
res
}
#[cold]
fn get_last_error() -> apperr::Error {
unsafe { gle_to_apperr(Foundation::GetLastError()) }
}
#[inline]
const fn gle_to_apperr(gle: u32) -> apperr::Error {
apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle })
}
#[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)
}

3853
pkgs/edit/src/tui.rs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Everything related to Unicode lives here.
mod measurement;
mod tables;
mod utf8;
pub use measurement::*;
pub use utf8::*;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,278 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::{hint, iter};
/// An iterator over UTF-8 encoded characters.
///
/// This differs from [`std::str::Chars`] in that it works on unsanitized
/// byte slices and transparently replaces invalid UTF-8 sequences with U+FFFD.
///
/// This follows ICU's bitmask approach for `U8_NEXT_OR_FFFD` relatively
/// closely. This is important for compatibility, because it implements the
/// WHATWG recommendation for UTF8 error recovery. It's also helpful, because
/// the excellent folks at ICU have probably spent a lot of time optimizing it.
#[derive(Clone, Copy)]
pub struct Utf8Chars<'a> {
source: &'a [u8],
offset: usize,
}
impl<'a> Utf8Chars<'a> {
/// Creates a new `Utf8Chars` iterator starting at the given `offset`.
pub fn new(source: &'a [u8], offset: usize) -> Self {
Self { source, offset }
}
/// Returns the byte slice this iterator was created with.
pub fn source(&self) -> &'a [u8] {
self.source
}
/// Checks if the source is empty.
pub fn is_empty(&self) -> bool {
self.source.is_empty()
}
/// Returns the length of the source.
pub fn len(&self) -> usize {
self.source.len()
}
/// Returns the current offset in the byte slice.
///
/// This will be past the last returned character.
pub fn offset(&self) -> usize {
self.offset
}
/// Sets the offset to continue iterating from.
pub fn seek(&mut self, offset: usize) {
self.offset = offset;
}
/// Returns true if `next` will return another character.
pub fn has_next(&self) -> bool {
self.offset < self.source.len()
}
// I found that on mixed 50/50 English/Non-English text,
// performance actually suffers when this gets inlined.
#[cold]
fn next_slow(&mut self, c: u8) -> char {
if self.offset >= self.source.len() {
return Self::fffd();
}
let mut cp = c as u32;
if cp < 0xE0 {
// UTF8-2 = %xC2-DF UTF8-tail
if cp < 0xC2 {
return Self::fffd();
}
// The lead byte is 110xxxxx
// -> Strip off the 110 prefix
cp &= !0xE0;
} else if cp < 0xF0 {
// UTF8-3 =
// %xE0 %xA0-BF UTF8-tail
// %xE1-EC UTF8-tail UTF8-tail
// %xED %x80-9F UTF8-tail
// %xEE-EF UTF8-tail UTF8-tail
// This is a pretty neat approach seen in ICU4C, because it's a 1:1 translation of the RFC.
// I don't understand why others don't do the same thing. It's rather performant.
const BITS_80_9F: u8 = 1 << 0b100; // 0x80-9F, aka 0b100xxxxx
const BITS_A0_BF: u8 = 1 << 0b101; // 0xA0-BF, aka 0b101xxxxx
const BITS_BOTH: u8 = BITS_80_9F | BITS_A0_BF;
const LEAD_TRAIL1_BITS: [u8; 16] = [
// v-- lead byte
BITS_A0_BF, // 0xE0
BITS_BOTH, // 0xE1
BITS_BOTH, // 0xE2
BITS_BOTH, // 0xE3
BITS_BOTH, // 0xE4
BITS_BOTH, // 0xE5
BITS_BOTH, // 0xE6
BITS_BOTH, // 0xE7
BITS_BOTH, // 0xE8
BITS_BOTH, // 0xE9
BITS_BOTH, // 0xEA
BITS_BOTH, // 0xEB
BITS_BOTH, // 0xEC
BITS_80_9F, // 0xED
BITS_BOTH, // 0xEE
BITS_BOTH, // 0xEF
];
// The lead byte is 1110xxxx
// -> Strip off the 1110 prefix
cp &= !0xF0;
let t = self.source[self.offset] as u32;
if LEAD_TRAIL1_BITS[cp as usize] & (1 << (t >> 5)) == 0 {
return Self::fffd();
}
cp = (cp << 6) | (t & 0x3F);
self.offset += 1;
if self.offset >= self.source.len() {
return Self::fffd();
}
} else {
// UTF8-4 =
// %xF0 %x90-BF UTF8-tail UTF8-tail
// %xF1-F3 UTF8-tail UTF8-tail UTF8-tail
// %xF4 %x80-8F UTF8-tail UTF8-tail
// This is similar to the above, but with the indices flipped:
// The trail byte is the index and the lead byte mask is the value.
// This is because the split at 0x90 requires more bits than fit into an u8.
const TRAIL1_LEAD_BITS: [u8; 16] = [
// --------- 0xF4 lead
// | ...
// | +---- 0xF0 lead
// v v
0b_00000, //
0b_00000, //
0b_00000, //
0b_00000, //
0b_00000, //
0b_00000, //
0b_00000, // trail bytes:
0b_00000, //
0b_11110, // 0x80-8F -> 0x80-8F can be preceded by 0xF1-F4
0b_01111, // 0x90-9F -v
0b_01111, // 0xA0-AF -> 0x90-BF can be preceded by 0xF0-F3
0b_01111, // 0xB0-BF -^
0b_00000, //
0b_00000, //
0b_00000, //
0b_00000, //
];
// The lead byte *may* be 11110xxx, but could also be e.g. 11111xxx.
// -> Only strip off the 1111 prefix
cp &= !0xF0;
// Now we can verify if it's actually <= 0xF4.
// Curiously, this if condition does a lot of heavy lifting for
// performance (+13%). I think it's just a coincidence though.
if cp > 4 {
return Self::fffd();
}
let t = self.source[self.offset] as u32;
if TRAIL1_LEAD_BITS[(t >> 4) as usize] & (1 << cp) == 0 {
return Self::fffd();
}
cp = (cp << 6) | (t & 0x3F);
self.offset += 1;
if self.offset >= self.source.len() {
return Self::fffd();
}
// UTF8-tail = %x80-BF
let t = (self.source[self.offset] as u32).wrapping_sub(0x80);
if t > 0x3F {
return Self::fffd();
}
cp = (cp << 6) | t;
self.offset += 1;
if self.offset >= self.source.len() {
return Self::fffd();
}
}
// SAFETY: All branches above check for `if self.offset >= self.source.len()`
// one way or another. This is here because the compiler doesn't get it otherwise.
unsafe { hint::assert_unchecked(self.offset < self.source.len()) };
// UTF8-tail = %x80-BF
let t = (self.source[self.offset] as u32).wrapping_sub(0x80);
if t > 0x3F {
return Self::fffd();
}
cp = (cp << 6) | t;
self.offset += 1;
// SAFETY: If `cp` wasn't a valid codepoint, we already returned U+FFFD above.
#[allow(clippy::transmute_int_to_char)]
unsafe {
char::from_u32_unchecked(cp)
}
}
// This simultaneously serves as a `cold_path` marker.
// It improves performance by ~5% and reduces code size.
#[cold]
#[inline(always)]
fn fffd() -> char {
'\u{FFFD}'
}
}
impl Iterator for Utf8Chars<'_> {
type Item = char;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
if self.offset >= self.source.len() {
return None;
}
let c = self.source[self.offset];
self.offset += 1;
// Fast-passing ASCII allows this function to be trivially inlined everywhere,
// as the full decoder is a little too large for that.
if (c & 0x80) == 0 {
// UTF8-1 = %x00-7F
Some(c as char)
} else {
// Weirdly enough, adding a hint here to assert that `next_slow`
// only returns codepoints >= 0x80 makes `ucd` ~5% slower.
Some(self.next_slow(c))
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
// Lower bound: All remaining bytes are 4-byte sequences.
// Upper bound: All remaining bytes are ASCII.
let remaining = self.source.len() - self.offset;
(remaining / 4, Some(remaining))
}
}
impl iter::FusedIterator for Utf8Chars<'_> {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_broken_utf8() {
let source = [b'a', 0xED, 0xA0, 0x80, b'b'];
let mut chars = Utf8Chars::new(&source, 0);
let mut offset = 0;
for chunk in source.utf8_chunks() {
for ch in chunk.valid().chars() {
offset += ch.len_utf8();
assert_eq!(chars.next(), Some(ch));
assert_eq!(chars.offset(), offset);
}
if !chunk.invalid().is_empty() {
offset += chunk.invalid().len();
assert_eq!(chars.next(), Some('\u{FFFD}'));
assert_eq!(chars.offset(), offset);
}
}
}
}

339
pkgs/edit/src/vt.rs Normal file
View file

@ -0,0 +1,339 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Our VT parser.
use std::{mem, time};
use crate::simd::memchr2;
/// The parser produces these tokens.
pub enum Token<'parser, 'input> {
/// A bunch of text. Doesn't contain any control characters.
Text(&'input str),
/// A single control character, like backspace or return.
Ctrl(char),
/// We encountered `ESC x` and this contains `x`.
Esc(char),
/// We encountered `ESC O x` and this contains `x`.
SS3(char),
/// A CSI sequence started with `ESC [`.
///
/// They are the most common escape sequences. See [`Csi`].
Csi(&'parser Csi),
/// An OSC sequence started with `ESC ]`.
///
/// The sequence may be split up into multiple tokens if the input
/// is given in chunks. This is indicated by the `partial` field.
Osc { data: &'input str, partial: bool },
/// An DCS sequence started with `ESC P`.
///
/// The sequence may be split up into multiple tokens if the input
/// is given in chunks. This is indicated by the `partial` field.
Dcs { data: &'input str, partial: bool },
}
/// Stores the state of the parser.
#[derive(Clone, Copy)]
enum State {
Ground,
Esc,
Ss3,
Csi,
Osc,
Dcs,
OscEsc,
DcsEsc,
}
/// A single CSI sequence, parsed for your convenience.
pub struct Csi {
/// The parameters of the CSI sequence.
pub params: [u16; 32],
/// The number of parameters stored in [`Csi::params`].
pub param_count: usize,
/// The private byte, if any. `0` if none.
///
/// The private byte is the first character right after the
/// `ESC [` sequence. It is usually a `?` or `<`.
pub private_byte: char,
/// The final byte of the CSI sequence.
///
/// This is the last character of the sequence, e.g. `m` or `H`.
pub final_byte: char,
}
pub struct Parser {
state: State,
// Csi is not part of State, because it allows us
// to more quickly erase and reuse the struct.
csi: Csi,
}
impl Parser {
pub fn new() -> Self {
Self {
state: State::Ground,
csi: Csi { params: [0; 32], param_count: 0, private_byte: '\0', final_byte: '\0' },
}
}
/// Suggests a timeout for the next call to `read()`.
///
/// We need this because of the ambiguouity of whether a trailing
/// escape character in an input is starting another escape sequence or
/// is just the result of the user literally pressing the Escape key.
pub fn read_timeout(&mut self) -> std::time::Duration {
match self.state {
// 100ms is a upper ceiling for a responsive feel. This uses half that,
// under the assumption that a really slow terminal needs equal amounts
// of time for I and O. Realistically though, this could be much lower.
State::Esc => time::Duration::from_millis(50),
_ => time::Duration::MAX,
}
}
/// Parses the given input into VT sequences.
///
/// You should call this function even if your `read()`
/// had a timeout (pass an empty string in that case).
pub fn parse<'parser, 'input>(
&'parser mut self,
input: &'input str,
) -> Stream<'parser, 'input> {
Stream { parser: self, input, off: 0 }
}
}
/// An iterator that parses VT sequences into [`Token`]s.
///
/// Can't implement [`Iterator`], because this is a "lending iterator".
pub struct Stream<'parser, 'input> {
parser: &'parser mut Parser,
input: &'input str,
off: usize,
}
impl<'parser, 'input> Stream<'parser, 'input> {
/// Returns the input that is being parsed.
pub fn input(&self) -> &'input str {
self.input
}
/// Returns the current parser offset.
pub fn offset(&self) -> usize {
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
}
/// Parses the next VT sequence from the previously given input.
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<Token<'parser, 'input>> {
// I don't know how to tell Rust that `self.parser` and its lifetime
// `'parser` outlives `self`, and at this point I don't care.
let parser = unsafe { mem::transmute::<_, &'parser mut Parser>(&mut *self.parser) };
let input = self.input;
let bytes = input.as_bytes();
// If the previous input ended with an escape character, `read_timeout()`
// returned `Some(..)` timeout, and if the caller did everything correctly
// and there was indeed a timeout, we should be called with an empty
// input. In that case we'll return the escape as its own token.
if input.is_empty() && matches!(parser.state, State::Esc) {
parser.state = State::Ground;
return Some(Token::Esc('\0'));
}
while self.off < bytes.len() {
match parser.state {
State::Ground => match bytes[self.off] {
0x1b => {
parser.state = State::Esc;
self.off += 1;
}
c @ (0x00..0x20 | 0x7f) => {
self.off += 1;
return Some(Token::Ctrl(c as char));
}
_ => {
let beg = self.off;
while {
self.off += 1;
self.off < bytes.len()
&& bytes[self.off] >= 0x20
&& bytes[self.off] != 0x7f
} {}
return Some(Token::Text(&input[beg..self.off]));
}
},
State::Esc => {
let c = bytes[self.off];
self.off += 1;
match c {
b'[' => {
parser.state = State::Csi;
parser.csi.private_byte = '\0';
parser.csi.final_byte = '\0';
while parser.csi.param_count > 0 {
parser.csi.param_count -= 1;
parser.csi.params[parser.csi.param_count] = 0;
}
}
b']' => {
parser.state = State::Osc;
}
b'O' => {
parser.state = State::Ss3;
}
b'P' => {
parser.state = State::Dcs;
}
c => {
parser.state = State::Ground;
return Some(Token::Esc(c as char));
}
}
}
State::Ss3 => {
parser.state = State::Ground;
let c = bytes[self.off];
self.off += 1;
return Some(Token::SS3(c as char));
}
State::Csi => {
loop {
// If we still have slots left, parse the parameter.
if parser.csi.param_count < parser.csi.params.len() {
let dst = &mut parser.csi.params[parser.csi.param_count];
while self.off < bytes.len() && bytes[self.off].is_ascii_digit() {
let add = bytes[self.off] as u32 - b'0' as u32;
let value = *dst as u32 * 10 + add;
*dst = value.min(u16::MAX as u32) as u16;
self.off += 1;
}
} else {
// ...otherwise, skip the parameters until we find the final byte.
while self.off < bytes.len() && bytes[self.off].is_ascii_digit() {
self.off += 1;
}
}
// Encountered the end of the input before finding the final byte.
if self.off >= bytes.len() {
return None;
}
let c = bytes[self.off];
self.off += 1;
match c {
0x40..=0x7e => {
parser.state = State::Ground;
parser.csi.final_byte = c as char;
if parser.csi.param_count != 0 || parser.csi.params[0] != 0 {
parser.csi.param_count += 1;
}
return Some(Token::Csi(&parser.csi as &'parser Csi));
}
b';' => parser.csi.param_count += 1,
b'<'..=b'?' => parser.csi.private_byte = c as char,
_ => {}
}
}
}
State::Osc | State::Dcs => {
let beg = self.off;
let mut data;
let mut partial;
loop {
// Find any indication for the end of the OSC/DCS sequence.
self.off = memchr2(b'\x07', b'\x1b', bytes, self.off);
data = &input[beg..self.off];
partial = self.off >= bytes.len();
// Encountered the end of the input before finding the terminator.
if partial {
break;
}
let c = bytes[self.off];
self.off += 1;
if c == 0x1b {
// It's only a string terminator if it's followed by \.
// We're at the end so we're saving the state and will continue next time.
if self.off >= bytes.len() {
parser.state = match parser.state {
State::Osc => State::OscEsc,
_ => State::DcsEsc,
};
partial = true;
break;
}
// False alarm: Not a string terminator.
if bytes[self.off] != b'\\' {
continue;
}
self.off += 1;
}
break;
}
let state = parser.state;
if !partial {
parser.state = State::Ground;
}
return match state {
State::Osc => Some(Token::Osc { data, partial }),
_ => Some(Token::Dcs { data, partial }),
};
}
State::OscEsc | State::DcsEsc => {
// We were processing an OSC/DCS sequence and the last byte was an escape character.
// It's only a string terminator if it's followed by \ (= "\x1b\\").
if bytes[self.off] == b'\\' {
// It was indeed a string terminator and we can now tell the caller about it.
let state = parser.state;
// Consume the terminator (one byte in the previous input and this byte).
parser.state = State::Ground;
self.off += 1;
return match state {
State::OscEsc => Some(Token::Osc { data: "", partial: false }),
_ => Some(Token::Dcs { data: "", partial: false }),
};
} else {
// False alarm: Not a string terminator.
// We'll return the escape character as a separate token.
// Processing will continue from the current state (`bytes[self.off]`).
parser.state = match parser.state {
State::OscEsc => State::Osc,
_ => State::Dcs,
};
return match parser.state {
State::Osc => Some(Token::Osc { data: "\x1b", partial: true }),
_ => Some(Token::Dcs { data: "\x1b", partial: true }),
};
}
}
}
}
None
}
}

View file

@ -0,0 +1,380 @@
# 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

@ -0,0 +1,12 @@
[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"] }

View file

@ -0,0 +1,15 @@
# Grapheme Table Generator
This tool processes Unicode Character Database (UCD) XML files to generate efficient, multi-stage trie lookup tables for properties relevant to terminal applications:
* Grapheme cluster breaking rules
* Line breaking rules (optional)
* Character width properties
## Usage
* Download [ucd.nounihan.grouped.zip](https://www.unicode.org/Public/UCD/latest/ucdxml/ucd.nounihan.grouped.zip)
* Run some equivalent of:
```sh
grapheme-table-gen --lang=rust --extended --no-ambiguous --line-breaks path/to/ucd.nounihan.grouped.xml
```
* Place the result in `src/unicode/tables.rs`

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,288 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// Used as an indicator in our rules for ÷ ("does not join").
// Underscore is one of the few characters that are permitted as an identifier,
// are monospace in most fonts and also visually distinct from the digits.
const X: i32 = -1;
// The following rules are based on the Grapheme Cluster Boundaries section of Unicode Standard Annex #29,
// but slightly modified to allow for use with a plain MxN lookup table.
//
// Break at the start and end of text, unless the text is empty.
// GB1: ~ sot ÷ Any
// GB2: ~ Any ÷ eot
// Handled by our ucd_* functions.
//
// Do not break between a CR and LF. Otherwise, break before and after controls.
// GB3: ✓ CR × LF
// GB4: ✓ (Control | CR | LF) ÷
// GB5: ✓ ÷ (Control | CR | LF)
//
// Do not break Hangul syllable or other conjoining sequences.
// GB6: ✓ L × (L | V | LV | LVT)
// GB7: ✓ (LV | V) × (V | T)
// GB8: ✓ (LVT | T) × T
//
// Do not break before extending characters or ZWJ.
// GB9: ✓ × (Extend | ZWJ)
//
// Do not break before SpacingMarks, or after Prepend characters.
// GB9a: ✓ × SpacingMark
// GB9b: ✓ Prepend ×
//
// Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker.
// GB9c: ~ \p{InCB=Linker} × \p{InCB=Consonant}
// × \p{InCB=Linker}
// modified from
// \p{InCB=Consonant} [ \p{InCB=Extend} \p{InCB=Linker} ]* \p{InCB=Linker} [ \p{InCB=Extend} \p{InCB=Linker} ]* × \p{InCB=Consonant}
// because this has almost the same effect from what I can tell for most text, and greatly simplifies our design.
//
// Do not break within emoji modifier sequences or emoji zwj sequences.
// GB11: ~ ZWJ × \p{Extended_Pictographic} modified from \p{Extended_Pictographic} Extend* ZWJ × \p{Extended_Pictographic}
// because this allows us to use LUTs, while working for most valid text.
//
// Do not break within emoji flag sequences. That is, do not break between regional indicator (RI) symbols if there is an odd number of RI characters before the break point.
// GB12: ~ sot (RI RI)* RI × RI
// GB13: ~ [^RI] (RI RI)* RI × RI
// the lookup table we generate supports RIs via something akin to RI ÷ RI × RI ÷ RI, but the corresponding
// grapheme cluster algorithm doesn't count them. It would need to be updated to recognize and special-case RIs.
//
// Otherwise, break everywhere.
// GB999: ✓ Any ÷ Any
//
// This is a great reference for the resulting table:
// https://www.unicode.org/Public/UCD/latest/ucd/auxiliary/GraphemeBreakTest.html
#[rustfmt::skip]
pub const JOIN_RULES_GRAPHEME_CLUSTER: [[[i32; 16]; 16]; 2] = [
// Base table
[
/* ↓ leading → trailing codepoint */
/* | Other | CR | LF | Control | Extend | RI | Prepend | HangulL | HangulV | HangulT | HangulLV | HangulLVT | InCBLinker | InCBConsonant | ExtPic | ZWJ | */
/* Other | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* CR | */ [X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
/* LF | */ [X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
/* Control | */ [X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
/* Extend | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* RI | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* Prepend | */ [0 /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */],
/* HangulL | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, 0 /* | */, 0 /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulV | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulT | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulLV | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulLVT | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* InCBLinker | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, 0 /* | */],
/* InCBConsonant | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* ExtPic | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* ZWJ | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, 0 /* | */, 0 /* | */],
],
// Once we have encountered a Regional Indicator pair we'll enter this table.
// It's a copy of the base table, but instead of RI × RI, we're RI ÷ RI.
[
/* ↓ leading → trailing codepoint */
/* | Other | CR | LF | Control | Extend | RI | Prepend | HangulL | HangulV | HangulT | HangulLV | HangulLVT | InCBLinker | InCBConsonant | ExtPic | ZWJ | */
/* Other | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* CR | */ [X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
/* LF | */ [X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
/* Control | */ [X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
/* Extend | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* RI | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* Prepend | */ [0 /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */, 0 /* | */],
/* HangulL | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, 0 /* | */, 0 /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulV | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulT | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulLV | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* HangulLVT | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* InCBLinker | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, 0 /* | */, X /* | */, 0 /* | */],
/* InCBConsonant | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* ExtPic | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, 0 /* | */],
/* ZWJ | */ [X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 0 /* | */, X /* | */, 0 /* | */, 0 /* | */],
],
];
// The following rules are based on Unicode Standard Annex #14: Line Breaking Properties,
// but heavily modified to allow for use with lookup tables.
//
// TODO: I should go through this and cross check: https://www.unicode.org/Public/draft/ucd/auxiliary/LineBreakTest.html
//
// NOTE: If you convert these rules into a lookup table, you must apply them in reverse order.
// This is because the rules are ordered from most to least important (e.g. LB8 overrides LB18).
//
// Resolve line breaking classes:
// LB1: Assign a line breaking class [...].
// ✗ Unicode does that for us via the "lb" attribute.
//
// Start and end of text:
// LB2: Never break at the start of text.
// ~ Functionality not needed.
// LB3: Always break at the end of text.
// ~ Functionality not needed.
//
// Mandatory breaks:
// LB4: Always break after hard line breaks.
// ~ Handled by our ucd_* functions.
// LB5: Treat CR followed by LF, as well as CR, LF, and NL as hard line breaks.
// ~ Handled by our ucd_* functions.
// LB6: Do not break before hard line breaks.
// ~ Handled by our ucd_* functions.
//
// Explicit breaks and non-breaks:
// LB7: Do not break before spaces or zero width space.
// ✓ × SP
// ✓ × ZW
// LB8: Break before any character following a zero-width space, even if one or more spaces intervene.
// ~ ZW ÷ modified from ZW SP* ÷ because it's not worth being this anal about accuracy here.
// LB8a: Do not break after a zero width joiner.
// ~ Our ucd_* functions never break within grapheme clusters.
//
// Combining marks:
// LB9: Do not break a combining character sequence; treat it as if it has the line breaking class of the base character in all of the following rules. Treat ZWJ as if it were CM.
// ~ Our ucd_* functions never break within grapheme clusters.
// LB10: Treat any remaining combining mark or ZWJ as AL.
// ✗ To be honest, I'm not entirely sure, I understand the implications of this rule.
//
// Word joiner:
// LB11: Do not break before or after Word joiner and related characters.
// ✓ × WJ
// ✓ WJ ×
//
// Non-breaking characters:
// LB12: Do not break after NBSP and related characters.
// ✓ GL ×
// LB12a: Do not break before NBSP and related characters, except after spaces and hyphens.
// ✓ [^SP BA HY] × GL
//
// Opening and closing:
// LB13: Do not break before ']' or '!' or '/', even after spaces.
// ✓ × CL
// ✓ × CP
// ✓ × EX
// ✓ × SY
// LB14: Do not break after '[', even after spaces.
// ~ OP × modified from OP SP* × just because it's simpler. It would be nice to address this.
// LB15a: Do not break after an unresolved initial punctuation that lies at the start of the line, after a space, after opening punctuation, or after an unresolved quotation mark, even after spaces.
// ✗ Not implemented. Seemed too complex for little gain?
// LB15b: Do not break before an unresolved final punctuation that lies at the end of the line, before a space, before a prohibited break, or before an unresolved quotation mark, even after spaces.
// ✗ Not implemented. Seemed too complex for little gain?
// LB15c: Break before a decimal mark that follows a space, for instance, in 'subtract .5'.
// ~ SP ÷ IS modified from SP ÷ IS NU because this fits neatly with LB15d.
// LB15d: Otherwise, do not break before ';', ',', or '.', even after spaces.
// ✓ × IS
// LB16: Do not break between closing punctuation and a nonstarter (lb=NS), even with intervening spaces.
// ✗ Not implemented. Could be useful in the future, but its usefulness seemed limited to me.
// LB17: Do not break within '——', even with intervening spaces.
// ✗ Not implemented. Terminal applications nor code use em-dashes much anyway.
//
// Spaces:
// LB18: Break after spaces.
// ✓ SP ÷
//
// Special case rules:
// LB19: Do not break before non-initial unresolved quotation marks, such as ' ” ' or ' " ', nor after non-final unresolved quotation marks, such as ' “ ' or ' " '.
// ~ × QU modified from × [ QU - \p{Pi} ]
// ~ QU × modified from [ QU - \p{Pf} ] ×
// We implement the Unicode 16.0 instead of 16.1 rules, because it's simpler and allows us to use a LUT.
// LB19a: Unless surrounded by East Asian characters, do not break either side of any unresolved quotation marks.
// ✗ [^$EastAsian] × QU
// ✗ × QU ( [^$EastAsian] | eot )
// ✗ QU × [^$EastAsian]
// ✗ ( sot | [^$EastAsian] ) QU ×
// Same as LB19.
// LB20: Break before and after unresolved CB.
// ✗ We break by default. Unicode inline objects are super irrelevant in a terminal in either case.
// LB20a: Do not break after a word-initial hyphen.
// ✗ Not implemented. Seemed not worth the hassle as the window will almost always be >1 char wide.
// LB21: Do not break before hyphen-minus, other hyphens, fixed-width spaces, small kana, and other non-starters, or after acute accents.
// ✓ × BA
// ✓ × HY
// ✓ × NS
// ✓ BB ×
// ✗ Added HY ÷ HY, because of the following note in TR14:
// > If used as hyphen, it acts like U+2010 HYPHEN, which has line break class BA.
// LB21a: Do not break after the hyphen in Hebrew + Hyphen + non-Hebrew.
// ✗ Not implemented. Perhaps in the future.
// LB21b: Do not break between Solidus and Hebrew letters.
// ✗ Not implemented. Perhaps in the future.
// LB22: Do not break before ellipses.
// ✓ × IN
//
// Numbers:
// LB23: Do not break between digits and letters.
// ✓ (AL | HL) × NU
// ✓ NU × (AL | HL)
// LB23a: Do not break between numeric prefixes and ideographs, or between ideographs and numeric postfixes.
// ✓ PR × (ID | EB | EM)
// ✓ (ID | EB | EM) × PO
// LB24: Do not break between numeric prefix/postfix and letters, or between letters and prefix/postfix.
// ✓ (PR | PO) × (AL | HL)
// ✓ (AL | HL) × (PR | PO)
// LB25: Do not break numbers:
// ~ CL × PO modified from NU ( SY | IS )* CL × PO
// ~ CP × PO modified from NU ( SY | IS )* CP × PO
// ~ CL × PR modified from NU ( SY | IS )* CL × PR
// ~ CP × PR modified from NU ( SY | IS )* CP × PR
// ~ ( NU | SY | IS ) × PO modified from NU ( SY | IS )* × PO
// ~ ( NU | SY | IS ) × PR modified from NU ( SY | IS )* × PR
// ~ PO × OP modified from PO × OP NU
// ~ PO × OP modified from PO × OP IS NU
// ✓ PO × NU
// ~ PR × OP modified from PR × OP NU
// ~ PR × OP modified from PR × OP IS NU
// ✓ PR × NU
// ✓ HY × NU
// ✓ IS × NU
// ~ ( NU | SY | IS ) × NU modified from NU ( SY | IS )* × NU
// Most were simplified because the cases this additionally allows don't matter much here.
//
// Korean syllable blocks
// LB26: Do not break a Korean syllable.
// ✗ Our ucd_* functions never break within grapheme clusters.
// LB27: Treat a Korean Syllable Block the same as ID.
// ✗ Our ucd_* functions never break within grapheme clusters.
//
// Finally, join alphabetic letters into words and break everything else.
// LB28: Do not break between alphabetics ("at").
// ✓ (AL | HL) × (AL | HL)
// LB28a: Do not break inside the orthographic syllables of Brahmic scripts.
// ✗ Our ucd_* functions never break within grapheme clusters.
// LB29: Do not break between numeric punctuation and alphabetics ("e.g.").
// ✓ IS × (AL | HL)
// LB30: Do not break between letters, numbers, or ordinary symbols and opening or closing parentheses.
// ✓ (AL | HL | NU) × [OP-$EastAsian]
// ✓ [CP-$EastAsian] × (AL | HL | NU)
// LB30a: Break between two regional indicator symbols if and only if there are an even number of regional indicators preceding the position of the break.
// ✗ Our ucd_* functions never break within grapheme clusters.
// LB30b: Do not break between an emoji base (or potential emoji) and an emoji modifier.
// ✗ Our ucd_* functions never break within grapheme clusters.
// LB31: Break everywhere else.
// ✗ Our default behavior.
#[rustfmt::skip]
pub const JOIN_RULES_LINE_BREAK: [[i32; 24]; 25] = [
/* ↓ leading → trailing codepoint */
/* | Other | WordJoiner | ZeroWidthSpace | Glue | Space | BreakAfter | BreakBefore | Hyphen | ClosePunctuation | CloseParenthesis_EA | CloseParenthesis_NotEA | Exclamation | Inseparable | Nonstarter | OpenPunctuation_EA | OpenPunctuation_NotEA | Quotation | InfixNumericSeparator | Numeric | PostfixNumeric | PrefixNumeric | SymbolsAllowingBreakAfter | Alphabetic | Ideographic | */
/* Other | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* WordJoiner | */ [1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */],
/* ZeroWidthSpace | */ [X /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
/* Glue | */ [1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */],
/* Space | */ [X /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* BreakAfter | */ [X /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* BreakBefore | */ [1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */],
/* Hyphen | */ [X /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* ClosePunctuation | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */],
/* CloseParenthesis_EA | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */],
/* CloseParenthesis_NotEA | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */],
/* Exclamation | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* Inseparable | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* Nonstarter | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* OpenPunctuation_EA | */ [1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */],
/* OpenPunctuation_NotEA | */ [1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */],
/* Quotation | */ [1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */],
/* InfixNumericSeparator | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */],
/* Numeric | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */],
/* PostfixNumeric | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */],
/* PrefixNumeric | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */],
/* SymbolsAllowingBreakAfter | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */],
/* Alphabetic | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */],
/* Ideographic | */ [X /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, 1 /* | */, X /* | */, X /* | */, 1 /* | */, 1 /* | */, X /* | */, 1 /* | */, X /* | */, 1 /* | */, X /* | */, X /* | */],
/* StartOfText | */ [X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */, X /* | */],
];